diff --git a/.github/workflows/sdk-wheels.yml b/.github/workflows/sdk-wheels.yml
index 834b3d7..1b70bff 100644
--- a/.github/workflows/sdk-wheels.yml
+++ b/.github/workflows/sdk-wheels.yml
@@ -6,6 +6,7 @@ on:
- 'sdk/**'
- 'sdk-abi/**'
- 'shield-swap-sdk/**'
+ - 'bridge-sdk/**'
- '.github/workflows/sdk-wheels.yml'
- '.github/workflows/sdk.yml'
branches:
@@ -18,6 +19,7 @@ on:
- 'sdk/**'
- 'sdk-abi/**'
- 'shield-swap-sdk/**'
+ - 'bridge-sdk/**'
- '.github/workflows/sdk-wheels.yml'
- '.github/workflows/sdk.yml'
workflow_dispatch:
@@ -271,17 +273,62 @@ jobs:
name: shield-swap-wheels-universal
path: shield-swap-sdk/dist
+ # aleo-bridge-sdk is pure Python (hatchling): one universal wheel + sdist.
+ # Tested against the built aleo-sdk wheel; installed WITH every extra for the
+ # suite and WITHOUT extras for the import smoke — an Aleo-only install must
+ # import (extras raise MissingExtraError at the point of use, never at import).
+ # Only the hermetic suite runs here (`-m "not live"`): no key, no live RPC.
+ build-bridge:
+ name: bridge-build
+ needs: [build]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - uses: actions/download-artifact@v4
+ with:
+ name: wheels-linux-x86_64
+ path: sdk-dist
+ - name: Build wheel + sdist
+ run: |
+ pip install build
+ python -m build bridge-sdk --outdir bridge-sdk/dist
+ - name: Test against the built aleo-sdk wheel (all extras, hermetic only)
+ run: |
+ pip install sdk-dist/aleo_sdk-*.whl
+ pip install "$(ls bridge-sdk/dist/*.whl)[evm,solana,mcp]" pytest pytest-asyncio
+ cd bridge-sdk && python -m pytest -q -m "not live"
+ - name: AGENTS.md up to date
+ run: python bridge-sdk/codegen/gen_context.py --check
+ - name: Smoke test wheel with extras
+ run: |
+ cd "$RUNNER_TEMP"
+ python -c "import aleo_bridge, aleo_bridge.eth, aleo_bridge.sol, aleo_bridge.mcp; print('aleo-bridge-sdk', aleo_bridge.__version__)"
+ python -m aleo_bridge | head -1
+ - name: Smoke test wheel WITHOUT extras
+ run: |
+ pip uninstall -y aleo-bridge-sdk web3 eth-account solders solana mcp
+ pip install "$(ls bridge-sdk/dist/*.whl)"
+ python -c "import aleo_bridge; from aleo_bridge import Bridge, Ethereum, Solana, DEFAULT_REGISTRY; print(len(DEFAULT_REGISTRY.routes(include_unavailable=True, environment=None)), 'routes')"
+ - name: Upload wheel
+ uses: actions/upload-artifact@v4
+ with:
+ name: bridge-wheels-universal
+ path: bridge-sdk/dist
+
# Publishing uses PyPI trusted publishing (OIDC) — no token secret. Each
# package has its own job because a pending publisher must be unique per
# (repo, workflow, environment): the job's environment name must exactly
- # match the publisher registered on PyPI for that project. All three gate
- # on every build job, and they release strictly in dependency order:
- # abi -> sdk -> shield-swap (shield-swap-sdk requires aleo-sdk on PyPI).
+ # match the publisher registered on PyPI for that project. All gate on
+ # every build job, and they release strictly in dependency order:
+ # abi -> sdk -> shield-swap / bridge (both require aleo-sdk on PyPI).
release-abi:
name: Release aleo-contract-abi-generator
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
- needs: [build, sdist, build-abi, sdist-abi, build-shield-swap]
+ needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, build-bridge]
environment: pypi-abi
permissions:
id-token: write
@@ -301,7 +348,7 @@ jobs:
name: Release aleo-sdk
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
- needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, release-abi]
+ needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, build-bridge, release-abi]
environment: pypi
permissions:
id-token: write
@@ -321,7 +368,7 @@ jobs:
name: Release shield-swap-sdk
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
- needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, release-sdk]
+ needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, build-bridge, release-sdk]
environment: pypi-shield-swap
permissions:
id-token: write
@@ -336,3 +383,23 @@ jobs:
with:
packages-dir: dist
skip-existing: true
+
+ release-bridge:
+ name: Release aleo-bridge-sdk
+ runs-on: ubuntu-latest
+ if: "startsWith(github.ref, 'refs/tags/')"
+ needs: [build, sdist, build-abi, sdist-abi, build-shield-swap, build-bridge, release-sdk]
+ environment: pypi-bridge
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: 'bridge-wheels-*'
+ merge-multiple: true
+ path: dist
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist
+ skip-existing: true
diff --git a/bridge-sdk/.gitignore b/bridge-sdk/.gitignore
new file mode 100644
index 0000000..5ce617b
--- /dev/null
+++ b/bridge-sdk/.gitignore
@@ -0,0 +1,5 @@
+.venv/
+dist/
+__pycache__/
+.pytest_cache/
+*.egg-info/
diff --git a/bridge-sdk/AGENTS.md b/bridge-sdk/AGENTS.md
new file mode 100644
index 0000000..93291cb
--- /dev/null
+++ b/bridge-sdk/AGENTS.md
@@ -0,0 +1,331 @@
+# aleo-bridge — agent guide
+
+> GENERATED from SDK docstrings by `codegen/gen_context.py` — do not
+> edit by hand; edit the docstrings and regenerate.
+
+Typed Python client that moves assets between Aleo, Ethereum and Solana
+over the reviewed Hyperlane warp routes and Circle xReserve deployments
+(`pip install aleo-bridge-sdk[evm,solana]`, imports as `aleo_bridge`).
+MCP alternative: `python -m aleo_bridge.mcp` exposes the same lifecycle as
+tools; `aleo_bridge.agent.bridge_tools()` gives Claude-shape tool schemas.
+Registry version `2026-08-31.solana-deposits.1`.
+
+## Tier 1 — the lifecycle (quote → execute → wait, then resume / complete as asked)
+
+```python
+from aleo_bridge import Bridge
+
+bridge = Bridge.from_env() # BRIDGE_PRIVATE_KEY (+ EVM/Solana keys) from the environment
+print(bridge.status()) # addresses, balances, pending transfers
+quote = bridge.quote("ethereum/wbtc", "aleo/wbtc", amount="0.001", recipient=bridge.aleo_address())
+print(quote.fees, quote.amount_out) # show these to the user BEFORE executing
+progress = bridge.execute(quote.plan) # source step; checkpoints saved to the bound store
+progress = bridge.wait(progress) # stops at resume / complete / done / failed
+if progress.next == "resume": progress = bridge.wait(bridge.resume(progress))
+if progress.next == "complete": progress = bridge.wait(bridge.complete(progress, secret_nonce=nonce))
+assert progress.next == "done", progress.error
+```
+
+### `from_env(**overrides: 'Any') -> "'Bridge'"`
+
+Everything from the environment (spec §3.3); writes nothing to disk. Overrides: ethereum, solana, registry, checkpoints.
+
+### `from_profile(home: 'Any' = None, *, network: 'str | None' = None, endpoint: 'str | None' = None, ethereum: 'Any' = None, solana: 'Any' = None) -> "'Bridge'"`
+
+The client for the local profile (spec §3.4), created on first use. *network*/*endpoint* apply only when
+creating. Side-chain connections come from the arguments or the same env variables as ``from_env``.
+
+### `status(self) -> 'BridgeStatus'`
+
+Read-only re-orientation: addresses and public balances of every registry asset per configured chain.
+Plan 4 fills ``pending`` from the checkpoint store.
+
+### `quote(self, source, destination, *, amount=None, amount_atomic=None, recipient: 'str', sender: 'str | None' = None, protocol: 'str | None' = None, mint_mode: 'str' = 'public', secret_nonce: 'str' = '0scalar')`
+
+Price a transfer and get the plan that ``execute`` takes. Nothing is signed.
+
+``source`` / ``destination`` are ``"chain/key"`` strings or ``(chain, key)``
+tuples (``"ethereum/usdc"``, ``"aleo/usdcx"``); give exactly one of
+``amount`` (human units, str) or ``amount_atomic`` (int). ``recipient`` is
+the destination-chain address. ``mint_mode`` (xReserve into Aleo only):
+``"public"`` balance, ``"record"`` minted by the relayer, or ``"private"``
+— you finish it yourself with ``complete`` and must keep ``secret_nonce``.
+Returns a kind-specific ``Quote`` (``quote.kind`` in evm-hyperlane /
+solana-hyperlane / aleo-hyperlane / evm-xreserve / aleo-xreserve) with
+``fees`` and ``amount_out`` in human units and ``quote.plan``. Show the
+user fees + amount before ``execute``.
+
+### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, gas_payment_microcredits: 'int | None' = None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0)`
+
+Commit funds on the source chain for ``quote.plan``; returns ``Progress``.
+
+Runs approval(s) → deposit / dispatch / burn, emitting a ``Checkpoint`` to
+``on_checkpoint`` (and the bound store) at every boundary — including
+AFTER proving and BEFORE broadcast for Aleo legs, so a crash there is
+resumable without proving twice. ``proving`` is ``"delegate"`` (DPS) or
+``"local"``; ``mode`` is ``"caller"|"signer"`` (Aleo Hyperlane) or
+``"private"|"public"|"public-as-signer"`` (Aleo xReserve burn, default
+private; ``record``/``merkle_proof`` optional — the SDK selects a record
+and computes the exclusion proof). The Hyperlane hook payment is
+re-quoted right before proving unless ``gas_payment_microcredits`` is
+pinned. Irreversible once the source step is broadcast: afterwards use
+``wait`` / ``recover``, never ``execute`` again.
+
+### `wait(self, progress, *, until=None, poll_seconds: 'float' = 15.0, timeout_seconds: 'float' = 1200.0, on_update=None, on_error=None, max_consecutive_errors: 'int' = 5)`
+
+Poll until the transfer finishes or needs you: stops at ``progress.next``
+in resume / complete / done / failed, or at any status in ``until``.
+
+A ``PollingTimeoutError`` is NOT a failure — the transfer is still in
+flight; call ``wait`` again or ``recover`` later. ``on_update`` receives
+each changed ``Progress``. A transient error (flaky RPC/HTTP transport)
+is retried up to ``max_consecutive_errors`` times, calling ``on_error``
+on each tolerated retry; a non-transient error propagates immediately.
+
+### `recover(self, checkpoint)`
+
+Rebuild ``Progress`` from a saved checkpoint (``Checkpoint``, dict or JSON) — reads only.
+
+Re-resolves the route from the live registry and reads chain state once;
+``progress.next`` then says what to do: ``wait``, ``resume``, ``complete``,
+``done`` or ``failed``.
+
+### `resume(self, progress, *, on_checkpoint=None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0, proving: 'str' = 'delegate')`
+
+Finish an interrupted source submission (``progress.next == "resume"``).
+
+Rebroadcasts the identical proved Aleo transaction (a duplicate response is
+success) or, on EVM, re-scans history and only then authorizes the single
+missing deposit/dispatch. Never repeats a confirmed step.
+
+### `complete(self, progress, *, secret_nonce: 'str', on_checkpoint=None, proving: 'str' = 'delegate')`
+
+Submit the private USDCx mint (``progress.next == "complete"``).
+
+Requires the same ``secret_nonce`` given to ``execute``; the SDK never
+stored it. Submits exactly one ``private_mint`` and returns
+``DESTINATION_CONFIRMING`` progress to ``wait`` on.
+
+### `pending(self) -> 'list'`
+
+The in-flight transfers of this profile — every checkpoint in the bound store,
+reconstructed offline (:func:`lifecycle.progress_from_checkpoint`): no network read, so one
+unreachable chain can never hide the others. A malformed checkpoint yields a ``Progress``
+with ``next == "failed"`` and ``error`` set instead of raising; call ``wait()``/``recover()``
+on any entry to refresh it against live chain state.
+
+## Serving a chatting user (the conversation pattern)
+
+### Keys and identity
+
+1. **NEVER ask the user to paste a private key into the conversation.** Keys
+ come from the environment only: `BRIDGE_PRIVATE_KEY` (Aleo),
+ `EVM_PRIVATE_KEY` + `ETHEREUM_RPC_URL`, `SOLANA_PRIVATE_KEY` (+ optional
+ `SOLANA_RPC_URL`), set in the user's own shell before the process starts.
+ `Bridge.from_profile()` creates an Aleo key on first use and never writes
+ EVM/Solana keys to disk.
+2. `status()` first in any session: which chains are configured, balances of
+ every bridge asset, and the pending transfers in the checkpoint store. A
+ pending transfer is finished with `recover` → `wait`/`resume`/`complete`,
+ never by starting a new one.
+
+### Quote first, always
+
+3. **Always `quote` before `execute`** and show the user the route, the fees
+ and `amount_out` in human units with symbols ("2 USDC → 2 USDCx; Hyperlane
+ hook payment 8.17 ALEO"), never raw atomic units. Minimums: xReserve
+ needs at least 2 USDC in and strictly more than the 2 USDCx withdrawal fee
+ out; Hyperlane moves one atomic unit but network fees and the relayer
+ payment cost more than that — say so.
+4. Only `execute` after the user confirms. Through the agent tools every
+ write requires `confirm=true`; without it the tool returns the quote and
+ moves nothing. A live mainnet execution additionally needs the user's
+ own `BRIDGE_LIVE_MAINNET_EXECUTE` acknowledgement — never set it yourself;
+ without it, treat any mainnet run as a rehearsal.
+
+### The source step is irreversible
+
+5. Once the deposit / dispatch / burn is broadcast the funds are committed.
+ A timeout, an RPC error or a crash after that point is an UNKNOWN outcome,
+ not a failure: recover from the last checkpoint (`recover(checkpoint)` or
+ `pending()`) — never run `execute` again for the same transfer. This is
+ the funds-safety rule above all others: never resend after an ambiguous
+ broadcast.
+
+### What `progress.next` means for the user
+
+| `progress.next` | Status | Tell the user | Do |
+| --- | --- | --- | --- |
+| `wait` | source confirming, attestation pending, delivery pending | "In flight; I'll keep checking." | `wait(progress)` (or re-check later from the checkpoint) |
+| `resume` | `SOURCE_SUBMISSION_PENDING` | "An approval confirmed / a proof was built but the transfer itself was not submitted; I can submit it now." | confirm, then `resume(progress)` |
+| `complete` | `DESTINATION_ACTION_REQUIRED` | "Circle attested your deposit; your private mint needs your signature (and the secret nonce)." | confirm, then `complete(progress, secret_nonce=...)` |
+| `done` | `COMPLETED` | "Delivered." Report source and destination transaction ids. | nothing |
+| `failed` | `FAILED` / `EXPIRED` | Relay `progress.error`; the source step did not commit funds or was rejected. | nothing — a new transfer needs a new quote |
+
+`wait` raising `PollingTimeoutError` is NOT a failure — say the transfer is
+still in flight and check again later.
+
+### Private mints and the secret nonce
+
+6. `mint_mode="private"` (USDC → USDCx) commits `(recipient, secret_nonce)` on
+ Ethereum. The same `secret_nonce` is required by `complete`; the SDK
+ **never stores** it and checkpoints exclude it (and every other secret).
+ Tell the user to keep it (the default `0scalar` needs no storage but adds
+ no entropy). Only the recipient's Aleo key can complete a private mint —
+ make sure the recipient IS the configured Aleo address before depositing.
+7. Aleo-origin Hyperlane transfers spend PUBLIC balances: `unshield` a private
+ record first. Hyperlane delivers into public balances; `shield` afterwards
+ if the user wants privacy. Private xReserve burns spend records directly.
+
+### While acting
+
+8. Writes are slow (proving + confirmation ≈ a minute or two on Aleo; Circle
+ attestation and Hyperlane relay take minutes). Never re-submit because a
+ call seems slow — `status()` / `recover` first.
+9. Confirm, act, report ids. Errors name their own fix — read the exception
+ message and do what it says.
+
+
+## Tier 2 — the protocol modules (building your own flows)
+
+Every Aleo write returns an `AleoCall`: nothing touches the network until
+`.simulate()` (free), `.prove()` / `.delegate_prepared()` (proved, not
+broadcast — checkpoint it), `.submit_prepared()`, `.transact()` (local
+proving + broadcast) or `.delegate()` (DPS + broadcast). EVM and Solana
+writes return `EvmCall` / `SolCall` with `.build()` (unsigned) and `.send()`.
+The lifecycle verbs above compose these; use them directly only when you
+need a single leg. Confirm-gated writes and the never-resend rule above
+apply here too — these are the same broadcasts, just one leg at a time.
+
+### `hyperlane.transfer_remote(self, asset: 'Any', recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, as_signer: 'bool' = False, gas_payment_microcredits: 'int | None' = None) -> 'AleoCall[DispatchReceipt]'`
+
+Withdraw an Aleo warp asset to Ethereum/Solana. Quotes the IGP payment now unless pinned; the
+lifecycle layer (plan 4) re-quotes at the last responsible moment by calling this again.
+
+### `hyperlane.quote_gas_payment(self, asset: 'Any') -> 'GasQuote'`
+
+Live relayer payment for the route (the exact u64 the hook asserts); quote right before proving.
+
+### `xreserve.burn(self, recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mode: 'str' = 'private', record: 'str | None' = None, merkle_proof: 'str | None' = None) -> 'AleoCall[BurnReceipt]'`
+
+Burn USDCx for USDC on Ethereum. ``private`` (default) spends a Token record via the wrapper and needs a
+freeze-list exclusion proof — both are resolved from chain state when not supplied. Minimum: more than
+the 2 USDCx withdrawal fee. The Aleo burn-attestation service forwards accepted burns to Circle.
+
+### `xreserve.private_mint(self, attestation: 'Attestation', recipient: 'str', *, secret_nonce: 'str' = '0scalar', route: 'Route | None' = None) -> 'AleoCall[MintReceipt]'`
+
+Finish a private-mode deposit: the only user-signed Aleo step of the inbound flow (``wrapper.private_mint``).
+
+### `xreserve.get_attestation(self, message_hash: "'str | bytes'", *, route: 'Route | None' = None) -> 'Attestation | None'`
+
+One Circle request for *message_hash*; ``None`` while pending (404).
+
+### `shield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`
+
+
+
+### `unshield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`
+
+
+
+### `freezelist.exclusion_proof(self, address: 'str', program: 'str') -> 'str'`
+
+``[MerkleProof; 2]`` proving *address* is not frozen on *program*; veil's empty pair when the list is empty.
+
+### `eth.transfer_remote(self, asset: 'Any' = None, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, plan: 'Plan | None' = None) -> 'EvmCall[DispatchReceipt]'`
+
+Send ETH, WBTC or USDT to Aleo through its Hyperlane Warp Route.
+
+Re-quotes ``quoteTransferRemote`` at send time. Collateral routes approve exactly the
+quoted token amount only when the allowance is short (USDT: a non-zero allowance is
+reset to 0 first). Native ETH sends amount + fee as ``msg.value``; collateral routes
+send the fee only. Each hash is checkpointed before polling; a timeout returns a
+pending ``DispatchReceipt``. The message id comes from the Mailbox ``DispatchId`` log.
+
+``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+re-resolved by id against the live registry, the sender must be the connected account, and
+the plan must equal what this call would have prepared itself. Mutually exclusive with ``asset=``.
+
+### `eth.deposit_usdc(self, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mint_mode: 'str | None' = None, secret_nonce: 'str' = '0scalar', plan: 'Plan | None' = None) -> 'EvmCall[DepositReceipt]'`
+
+Deposit USDC into Circle xReserve for USDCx on Aleo (minimum 2 USDC; irreversible once confirmed).
+
+``mint_mode``: ``public`` (public USDCx balance), ``record`` (protocol-minted private
+record), or ``private`` (deposit addressed to the shielded wrapper program; you must later
+run ``bridge.xreserve.private_mint`` / plan 4's ``complete`` with the same ``secret_nonce``,
+which the SDK never stores). Approves exactly the amount only when the allowance is
+short, then ``depositToRemote`` with no ``msg.value``. The confirmed ``DepositReceipt``
+carries Circle's message hash (receipt id) and the deposit nonce. ``mint_mode`` defaults to
+``plan.mint_mode`` when a plan is given, else ``"public"``.
+
+``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+re-resolved by id against the live registry, the sender must be the connected account, and
+the plan must equal what this call would have prepared itself. ``secret_nonce`` is never
+part of a plan, so a private deposit must still pass the same one it was quoted with.
+
+### `eth.quote_transfer_remote(self, asset: 'Any' = None, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, route: 'Route | None' = None, sender: 'str | None' = None, plan: 'Plan | None' = None) -> 'EvmHyperlaneQuote'`
+
+Quote an Ethereum → Aleo Hyperlane transfer without signing.
+
+Native routes (ETH): ``msg.value`` carries the asset and the relayer fee, so
+``native_fee_atomic = native_value_atomic - amount``. Collateral routes (WBTC, USDT):
+``msg.value`` is fee only and ``approval_required`` reflects the router's ERC-20
+allowance for ``sender`` (or the connection's account); it is ``None`` when no account is known.
+
+``plan=`` re-quotes a plan prepared earlier: it supplies the route, sender, recipient and
+amount, and is validated against the live registry. It is mutually exclusive with
+``asset=``/``route=``/``sender=``.
+
+### `sol.transfer_remote(self, recipient: 'str | None' = None, *, amount: 'str | None' = None, amount_atomic: 'int | None' = None, plan: 'Plan | None' = None) -> 'SolCall[DispatchReceipt]'`
+
+Send native SOL to an Aleo address over the Hyperlane warp route (spec §6).
+
+Returns a :class:`SolCall`: ``build()`` previews the partially signed transaction,
+``send()`` moves funds (amount + IGP payment + network fee + rent leave the wallet).
+
+``plan`` (from ``Bridge.execute``) supplies recipient and amount and must have been prepared for
+the connected wallet; its registry version and route id are re-checked against the live registry
+when the call runs. An ``amount``/``amount_atomic`` that disagrees with the plan is a
+``ValueError``. Without a plan, ``recipient`` is required.
+
+### `sol.quote_transfer_remote(self, recipient: 'str | None' = None, *, amount: 'str | None' = None, amount_atomic: 'int | None' = None, sender: 'str | None' = None, plan: 'Plan | None' = None) -> 'SolanaHyperlaneQuote'`
+
+Lamports required for a SOL → Aleo transfer: amount + IGP payment + network fee + rent (spec §5 kind
+``solana-hyperlane``). Reads Solana; never signs. ``sender`` defaults to the connected wallet and is required
+for the fee estimate.
+
+``plan`` (from ``Bridge.quote``) supplies recipient, amount and sender, and must match the live registry
+version and route; like ``EthModule`` it is mutually exclusive with ``sender=``, and an ``amount``/
+``amount_atomic`` that disagrees with the plan is a ``ValueError`` (an identical one is tolerated, so
+re-stating the plan's own amount is harmless). Without a plan, ``recipient`` is required.
+
+### Routes in the pinned registry
+
+| Route id | Protocol | Environment | Availability |
+| --- | --- | --- | --- |
+| `xreserve:ethereum/usdc->aleo/usdcx` | xreserve | mainnet | active |
+| `xreserve:aleo/usdcx->ethereum/usdc` | xreserve | mainnet | active |
+| `xreserve:sepolia/usdc->aleo-testnet/usdcx` | xreserve | testnet | active |
+| `xreserve:aleo-testnet/usdcx->sepolia/usdc` | xreserve | testnet | active |
+| `hyperlane:ethereum/eth->aleo/eth` | hyperlane | mainnet | active |
+| `hyperlane:aleo/eth->ethereum/eth` | hyperlane | mainnet | active |
+| `hyperlane:ethereum/wbtc->aleo/wbtc` | hyperlane | mainnet | active |
+| `hyperlane:aleo/wbtc->ethereum/wbtc` | hyperlane | mainnet | active |
+| `hyperlane:ethereum/usdt->aleo/usdt` | hyperlane | mainnet | active |
+| `hyperlane:aleo/usdt->ethereum/usdt` | hyperlane | mainnet | active |
+| `hyperlane:solana/sol->aleo/sol` | hyperlane | mainnet | active |
+| `hyperlane:aleo/sol->solana/sol` | hyperlane | mainnet | active |
+| `hyperlane:aleo/aleo->ethereum/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:ethereum/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->solana/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:solana/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->base/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:base/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->hyperevm/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:hyperevm/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:ethereum/usad->aleo/usad` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/usad->ethereum/usad` | hyperlane | mainnet | metadata-required |
+
+`metadata-required` routes are listed but refused by `quote`/`execute`
+until their deployments are reviewed upstream.
+
diff --git a/bridge-sdk/README.md b/bridge-sdk/README.md
new file mode 100644
index 0000000..0264ae9
--- /dev/null
+++ b/bridge-sdk/README.md
@@ -0,0 +1,463 @@
+# aleo-bridge-sdk
+
+Move assets between **Aleo**, **Ethereum** and **Solana** from Python, over the
+reviewed Hyperlane warp routes (ETH, WBTC, USDT, SOL) and Circle xReserve
+(USDC ↔ USDCx). Web3.py idioms on top of the `aleo` facade: reads return
+values, writes return prepared calls, the lifecycle is `quote → execute →
+wait`, and `recover` / `resume` / `complete` pick up wherever a process died.
+It is a port of veil's `@provablehq/aleo-bridge-sdk` 0.1.0 (registry
+`2026-08-31.solana-deposits.1`) into the web3.py-style verb structure of
+`aleo-sdk`.
+
+```sh
+pip install aleo-bridge-sdk # Aleo legs only
+pip install 'aleo-bridge-sdk[evm]' # + Ethereum (web3, eth-account)
+pip install 'aleo-bridge-sdk[solana]' # + Solana (solders, solana)
+pip install 'aleo-bridge-sdk[evm,solana]' # everything
+```
+
+Import name: `aleo_bridge`.
+
+## Quick start
+
+```python
+from aleo_bridge import Bridge
+
+bridge = Bridge.from_env() # keys + RPCs from the environment (table below)
+print(bridge.status()) # addresses, balances of every bridge asset, pending transfers
+
+quote = bridge.quote("ethereum/wbtc", "aleo/wbtc", amount="0.001", recipient=bridge.aleo_address())
+print(quote.kind, quote.fees, quote.amount_out) # show fees + amount_out before moving anything
+
+progress = bridge.execute(quote.plan) # approval (if needed) + dispatch; checkpoints saved
+progress = bridge.wait(progress) # stops at resume / complete / done / failed
+if progress.next == "resume": progress = bridge.wait(bridge.resume(progress))
+if progress.next == "complete": progress = bridge.wait(bridge.complete(progress, secret_nonce="…"))
+assert progress.next == "done", progress.error
+```
+
+`Bridge.from_profile()` instead keeps an Aleo key and a checkpoint store under
+`~/.aleo-bridge/` (`$ALEO_BRIDGE_HOME`), created on first use — EVM/Solana keys
+still come from the arguments or the environment and are never written to disk.
+
+## Connections
+
+```python
+from aleo import Aleo, HTTPProvider
+from aleo_bridge import Bridge, Ethereum, Solana
+
+aleo = Aleo(HTTPProvider("https://edge.provable.com/api", network="mainnet"))
+aleo.default_account = aleo.account.from_private_key(aleo_key)
+
+bridge = Bridge(aleo) # Aleo legs only
+bridge = Bridge(aleo, ethereum=Ethereum(ETH_RPC, private_key=evm_key),
+ solana=Solana(SOL_RPC, private_key=sol_key)) # raw keys
+bridge = Bridge(aleo, ethereum=Ethereum(w3=my_w3, signer=my_local_account),
+ solana=Solana(client=my_client, signer=my_keypair)) # configured signers
+bridge = Bridge(aleo, ethereum=my_w3, solana=my_client) # bare clients: read-only
+```
+
+| `Ethereum(...)` | Transport | Signer |
+| --- | --- | --- |
+| `rpc_url` + `private_key` / `signer` | `Web3(HTTPProvider(rpc_url))` | the key or `LocalAccount` |
+| `w3` + `private_key` / `signer` | your `Web3` (middleware, PoA, retries) | the key or `LocalAccount` |
+| `w3` alone | your `Web3` | `w3.eth.default_account` via your signing middleware, else read-only |
+
+| `Solana(...)` | Transport | Signer |
+| --- | --- | --- |
+| `rpc_url` (default `api.mainnet-beta.solana.com`) + `private_key` / `signer` | the SDK's own synchronous `SolanaRpcClient(rpc_url, commitment="confirmed")` — a `requests`-based JSON-RPC client, since solana-py ≥ 0.36 ships only an async client | base58 (Phantom export) / the 64-int `id.json` array, a solders `Keypair`, or any solana-py `Signer` |
+| `client` + `private_key` / `signer` | your client reused: a `SolanaRpcClient`, a solana-py `AsyncClient` (adapted internally through `_AsyncClientAdapter`), or any duck-typed object with the same RPC methods | as above |
+| `client` alone | your client | read-only |
+
+`Solana` supports `close()` and use as a context manager (`with Solana(...) as
+solana:`) to release the wrapped client's resources; `__exit__` swallows
+whatever `close()` raises, so call it directly if you need to see the error.
+`Bridge.from_env()` reads `EVM_PRIVATE_KEY` + `ETHEREUM_RPC_URL` and
+`SOLANA_PRIVATE_KEY` (+ `SOLANA_RPC_URL`) — see the aliases and precedence
+notes in the environment table below.
+
+## Routes
+
+Assets are `"chain/key"` (`"ethereum/usdc"`, `"aleo/usdcx"`); `bridge.registry`
+lists everything. Active mainnet routes:
+
+| Route | Protocol | Minimum | Notes |
+| --- | --- | --- | --- |
+| `xreserve:ethereum/usdc->aleo/usdcx` | Circle xReserve | 2 USDC | `mint_mode` public / record / **private** (you finish with `complete`) |
+| `xreserve:aleo/usdcx->ethereum/usdc` | Circle xReserve | > 2 USDCx | 2 USDCx withdrawal fee; private burn (default) needs a record + exclusion proof — computed for you |
+| `hyperlane:ethereum/eth->aleo/eth` / reverse | Hyperlane (native) | 1 wei | `msg.value` carries ETH + relayer fee |
+| `hyperlane:ethereum/wbtc->aleo/wbtc` / reverse | Hyperlane (collateral) | 1 sat | approval + dispatch |
+| `hyperlane:ethereum/usdt->aleo/usdt` / reverse | Hyperlane (collateral) | 1 µUSDT | approval reset to 0 first (USDT) |
+| `hyperlane:solana/sol->aleo/sol` / `hyperlane:aleo/sol->solana/sol` | Hyperlane | 1 lamport | IGP + rent quoted live |
+
+Testnet: `xreserve:sepolia/usdc->aleo-testnet/usdcx` and its reverse. ALEO and
+USAD routes, plus every `base`/`hyperevm` route, are `metadata-required`:
+listed, refused by `quote`/`execute` until their deployments are reviewed
+upstream.
+
+Aleo-origin Hyperlane transfers spend PUBLIC balances (`unshield` first);
+Hyperlane delivers into public balances (`shield` afterwards if you want).
+
+**Registry fee vs. live fee — a known discrepancy.** The registry's
+`xreserve:*usdcx->*usdc` literal is `withdrawalFeeAtomic = 2_000_000` (2
+USDCx), and `quote` promises exactly that. The 2026-09-18 testnet round trip
+(next section) actually delivered a live xReserve withdrawal fee of
+**≈1.0035 USDC**, not 2 USDC — Circle's fee is evidently dynamic and the
+registry literal has not been re-measured against it. This SDK does not
+change the registry literal or the quote; `amount_out` is a quote, not a
+guarantee, and the same literal is used on the mainnet route, so budget for
+the same gap there until someone re-measures it live.
+
+## The lifecycle
+
+```python
+quote = bridge.quote(source, destination, amount="…" | amount_atomic=…, recipient=…,
+ sender=None, protocol=None, mint_mode="public", secret_nonce="0scalar")
+progress = bridge.execute(quote.plan, on_checkpoint=save, proving="delegate", mode=None,
+ record=None, merkle_proof=None, gas_payment_microcredits=None,
+ secret_nonce=None, poll_seconds=1.0, timeout_seconds=120.0)
+progress = bridge.wait(progress, until=None, poll_seconds=15.0, timeout_seconds=1200.0, on_update=None)
+receipt = bridge.get_status(plan, receipt) # one refresh, no polling
+progress = bridge.recover(checkpoint) # reads only
+progress = bridge.resume(progress, on_checkpoint=save)
+progress = bridge.complete(progress, secret_nonce="…", on_checkpoint=save)
+bridge.pending() # recover() every stored checkpoint
+```
+
+`execute` emits a `Checkpoint` at every boundary: after each approval hash,
+after proving and **before** broadcast for Aleo legs, and after broadcast.
+Aleo legs prove through the delegated prover (`proving="delegate"`) or locally
+(`proving="local"`) and are broadcast from the checkpointed bytes, so a crash
+between proving and broadcast is resumable without proving twice. Every EVM
+and Solana write checkpoints its own hash **before** polling for the receipt,
+so a crash mid-poll never re-signs; `send()` is single-use per transfer —
+never call it twice for the same plan, recover from the checkpoint instead.
+
+| `progress.next` | Status | Meaning | You do |
+| --- | --- | --- | --- |
+| `wait` | source confirming / attestation pending / delivery pending | in flight | `wait(progress)` |
+| `resume` | `SOURCE_SUBMISSION_PENDING` | approval confirmed or proof built, transfer not submitted | `resume(progress)` |
+| `complete` | `DESTINATION_ACTION_REQUIRED` | Circle attested; your private mint needs your signature | `complete(progress, secret_nonce=…)` |
+| `done` | `COMPLETED` | delivered (destination Mailbox / nullifier / balance verified) | nothing |
+| `failed` | `FAILED` / `EXPIRED` | see `progress.error` | new quote |
+
+A `PollingTimeoutError` from `wait` is **not** a failure: the transfer is still
+in flight (`exc.progress`); call `wait` again or `recover` later. This holds
+for every leg — Ethereum, Solana and Aleo receipts all return "pending", never
+raise, on a polling timeout; a receipt timeout is never treated as a delivery
+failure. Once the deposit / dispatch / burn is broadcast, never call `execute`
+(or `send()`/`build()` again for the same call) for the same transfer —
+recover from the checkpoint.
+
+## Recovery
+
+```python
+from aleo_bridge import FileCheckpointStore
+bridge = Bridge.from_env(checkpoints=FileCheckpointStore("~/.aleo-bridge/checkpoints")) # or BRIDGE_CHECKPOINT_DIR
+for progress in bridge.pending(): # after a restart
+ if progress.next == "wait": progress = bridge.wait(progress)
+ if progress.next == "resume": progress = bridge.wait(bridge.resume(progress))
+ if progress.next == "complete": progress = bridge.wait(bridge.complete(progress, secret_nonce=my_nonce))
+```
+
+Checkpoints are an allowlist (version 1): intent, route id + registry version,
+transaction ids, the proved Aleo transaction, the Solana blockhash lifetime and
+the delivery baseline. Never keys, record plaintext, `secret_nonce`,
+attestations, payloads or hashes. `recover` re-resolves the route from the live
+registry and refuses a registry-version mismatch. Idempotent rebroadcast: a
+duplicate-transaction response from the node is success. `FileCheckpointStore`
+writes mode-600 files via an atomic rename; profile checkpoints (under
+`~/.aleo-bridge/`) get the same treatment. `Bridge(checkpoints=store)` also
+auto-saves through every lifecycle call, the same channel Ethereum, Solana and
+Aleo writes all use — you don't have to pass `on_checkpoint=` yourself unless
+you want a second sink.
+
+## Private USDCx mints
+
+`mint_mode="private"` commits `(recipient, secret_nonce)` on Ethereum; only the
+recipient's Aleo key can `complete` the mint, with the same `secret_nonce`. The
+SDK never stores the nonce (default `0scalar`). Public and record mints are
+relayer-driven and finish at `done` when the bridge nullifier confirms delivery.
+
+## shield / unshield
+
+```python
+bridge.shield("aleo/eth", amount="0.001").delegate() # public balance → private record (ARC-20)
+bridge.unshield("aleo/usdcx", amount="5").delegate() # ARC-22: record + freeze-list exclusion proof, computed for you
+bridge.freezelist.exclusion_proof(address, "usdcx_stablecoin.aleo") # the `[MerkleProof; 2]` literal itself
+```
+
+## Tier 2 modules
+
+`bridge.hyperlane.transfer_remote / quote_gas_payment / is_delivered`,
+`bridge.xreserve.burn / private_mint / get_attestation / is_delivered / hook_data`,
+`bridge.eth.transfer_remote / deposit_usdc / quote_transfer_remote / quote_deposit_usdc / balance / is_delivered
+/ source_status / recover_source`,
+`bridge.sol.transfer_remote / quote_transfer_remote / balance`. Aleo writes return an
+`AleoCall` (`simulate() / prove() / delegate_prepared() / submit_prepared() /
+transact() / delegate()`); EVM and Solana writes return `EvmCall` / `SolCall`
+(`build()` → unsigned, `send()`). `python -m aleo_bridge` prints the full
+generated reference (`AGENTS.md`).
+
+### Ethereum, directly
+
+```python
+quote = bridge.eth.quote_transfer_remote("wbtc", aleo_recipient, amount="0.001")
+print(quote.native_fee_atomic, quote.approval_required)
+
+call = bridge.eth.transfer_remote("wbtc", aleo_recipient, amount="0.001")
+call.build() # unsigned tx dicts: approve(s) then transferRemote
+result = call.send(on_checkpoint=store.save) # approvals → dispatch; each hash checkpointed before polling
+result.message_id, result.receipt.status # Hyperlane message id, DELIVERY_PENDING
+
+usdc_quote = bridge.eth.quote_deposit_usdc(aleo_recipient, amount="2", mint_mode="public")
+deposit = bridge.eth.deposit_usdc(aleo_recipient, amount="2", mint_mode="public").send()
+deposit.message_hash # Circle attestation lookup key (receipt id), ATTESTATION_PENDING
+
+bridge.eth.balance("eth"); bridge.eth.is_delivered(message_id) # reads
+bridge.eth.source_status(plan, receipt) # one refresh of an approval/confirming receipt
+bridge.eth.recover_source(plan, checkpoint) # log-scan recovery, never signs
+```
+
+Routes: ETH (native), WBTC and USDT (collateral; USDT resets a non-zero
+allowance to 0 first) via Hyperlane; USDC → USDCx via Circle xReserve (2 USDC
+minimum, `mint_mode` public/record/private — private deposits go to the
+shielded wrapper program and need the same `secret_nonce` at `complete` time;
+the SDK never stores it). A receipt timeout returns a pending receipt, never a
+failure. Live checks: `BRIDGE_LIVE_READS=1 ETHEREUM_RPC_URL=…` for read-only
+mainnet quotes (`tests/live/test_eth_reads.py`); `BRIDGE_LIVE_FUNDS=1
+BRIDGE_LIVE_STATE_DIR=… SEPOLIA_RPC_URL=… EVM_PRIVATE_KEY=…
+ALEO_E2E_PRIVATE_KEY=…` for the 2 USDC Sepolia leg
+(`tests/live/test_eth_sepolia_leg1.py`).
+
+### Solana, directly
+
+```python
+quote = bridge.sol.quote_transfer_remote(aleo_addr, amount="0.01") # amount + IGP + fee + rent, in lamports
+call = bridge.sol.transfer_remote(aleo_addr, amount="0.01")
+tx = call.build() # VersionedTransaction, unique-message key signed
+result = call.send(on_checkpoint=store.save) # fee-payer signature, broadcast, poll to confirmed
+result.message_id # Hyperlane message id from the Mailbox log
+```
+
+`private_key` accepts a base58 secret (Phantom export) or the 64-int JSON
+array of a solana-cli `id.json`. Every read uses confirmed commitment; the
+transaction sets a 400,000 compute-unit limit; the `SOURCE_CONFIRMING` receipt
+(signature, unique-message address, blockhash, last valid block height) is
+checkpointed before polling, and a polling timeout returns the pending receipt
+rather than failing. The instruction encoding and account list are pinned
+byte-for-byte against a recorded mainnet transfer
+(`tests/fixtures/sealevel-transfer-remote.json`).
+
+Live read-only checks (no key, no funds):
+`BRIDGE_LIVE_READS=1 .venv/bin/python -m pytest -m live tests/live/test_sol_reads.py -q -s`
+decodes the live IGP account and prints a leg-11 quote for a pinned sender;
+`SOLANA_RPC_URL` overrides the public default if it rate-limits. The funded
+round trip runs from `scripts/rehearse.py` (see "Live rehearsal" below).
+
+## Agents and MCP
+
+```python
+from aleo_bridge import bridge_tools, dispatch_tool
+tools = bridge_tools() # Claude `tools=` shape; bridge_tools(include_writes=False) for read-only
+dispatch_tool(bridge, "bridge_quote", {"source": "ethereum/usdc", "destination": "aleo/usdcx", "amount": "2", "recipient": addr})
+```
+
+Reads: `bridge_status`, `bridge_list_assets`, `bridge_list_routes`, `bridge_quote`,
+`bridge_get_progress`, `bridge_pending`. Writes (`bridge_execute`, `bridge_resume`,
+`bridge_complete`, `bridge_shield`, `bridge_unshield`) require `confirm: true` —
+without it they return the quote (or recovered progress / built call) plus
+`how_to_confirm`, and move nothing. `bridge_execute` takes the quote inputs and
+re-quotes internally, so agents never carry plans.
+`python -m aleo_bridge.mcp` serves the same tools over stdio (`[mcp]` extra).
+
+## Live tests and rehearsal
+
+The funded suite (`tests/live/`, ported from veil's `test/integration/live/`)
+moves real money. It is off unless you turn it on, in your own shell, one
+command at a time. `scripts/rehearse.py` drives the same case functions the
+live pytest suite uses, over one route or a whole case, at minimum amounts,
+and backs both the CLI and `-m live` so they cannot drift.
+
+**The testnet round trip has run for real, end to end, both directions**
+(2026-09-18): Sepolia deposit of 3 USDC
+(`0x08cd56e4a10c84d62ee000ceb20d854d2f1b6b9a6102db8f1414abfb871faab4`) into a
+private USDCx mint on aleo-testnet
+(`at1jdy6wyal4ndwwydy80hjxt5q9eh332vk6t8zrw8jsvgc0pg7jcxqc85day`), then a
+return burn
+(`at1cufuy7v4lc4dn5ek3vdfa0tpeh8rrqqnahyelv06d2ll5n0kvczsrv9ugq`) delivering a
+Sepolia withdrawal
+(`0xbacb3ff270f6cee401cf2aa9ce6ed3be3b9d0426b18c04a2674c86dca11e4b54`).
+Recovery from the on-disk checkpoint was exercised on both legs — the process
+that submitted was discarded and a fresh `Bridge` over the same
+`FileCheckpointStore` finished each transfer from `bridge.pending()`. The
+observed withdrawal fee (see "Registry fee vs. live fee" above) is the one
+concrete correction those runs produced. Every mainnet fund-moving case has
+been quoted and prechecked live (funding table below) but still runs
+quote-only — mainnet execution needs the operator to fund the bridge wallets
+first, which has not happened yet; this README does not carry wallet
+addresses or balances.
+
+**Gates** (read only — nothing in this repository sets them; the exact
+acknowledgement strings are the constants in `tests/live/config.py`):
+
+| Variable | Effect |
+| --- | --- |
+| `BRIDGE_LIVE_FUNDS=1` + `BRIDGE_LIVE_STATE_DIR=
` | funded cases exist at all |
+| `BRIDGE_LIVE_MAINNET_ACK=…` + `BRIDGE_LIVE_MAINNET_CASES=` | the named mainnet cases may run |
+| `BRIDGE_LIVE_MAINNET_EXECUTE=…` | the wallet may actually submit |
+
+Without the last one every case runs to the quote, prints the route/amount/fee
+table and returns — that is the default, and it is how you rehearse. Keys and
+endpoints come from `Bridge.from_env()` (`BRIDGE_PRIVATE_KEY`,
+`EVM_PRIVATE_KEY`/`BRIDGE_EVM_PRIVATE_KEY`, `SOLANA_PRIVATE_KEY`/
+`BRIDGE_SOLANA_PRIVATE_KEY`, `ETHEREUM_RPC_URL`/`BRIDGE_LIVE_ETHEREUM_RPC_URL`,
+`SOLANA_RPC_URL`/`BRIDGE_LIVE_SOLANA_RPC_URL`); recipients default to your own
+addresses and can be overridden with `BRIDGE_LIVE_ALEO_MAINNET_RECIPIENT` /
+`BRIDGE_LIVE_EVM_RECIPIENT` / `BRIDGE_LIVE_SOLANA_RECIPIENT`.
+
+**State.** Each case keeps one file at
+`$BRIDGE_LIVE_STATE_DIR//-.json` (mode 600) holding
+the checkpoint, the source/destination transaction ids and the message id;
+the private-mint secret nonce lives beside it in `.secret` (mode 600,
+created exclusively) and never in the state, a log or a checkpoint.
+Re-running a case resumes from that file — recover first, then
+`wait`/`resume`/`complete`; a completed case re-asserts what it recorded and
+exits. A timeout is *pending*, not a failure: the checkpoint stays on disk
+and the run prints the `--recover` command.
+
+**Cases and routes.** Five cases, each parametrized over every registry route
+it covers — both directions are separate cases, and a route with
+`availability != "active"` is reported as skipped-by-registry rather than
+dropped:
+
+| Case | Routes | Amount |
+| --- | --- | --- |
+| `evm-hyperlane` | ethereum → aleo (ETH, WBTC, USDT) | one atomic unit |
+| `aleo-hyperlane` | aleo → ethereum (ETH, WBTC, USDT), aleo → solana (SOL) | one atomic unit, `mode="signer"` |
+| `solana-hyperlane` | solana → aleo (SOL) | 1 lamport |
+| `evm-xreserve` | ethereum USDC → aleo USDCx | `2` USDC, private mint (needs `complete`) |
+| `aleo-xreserve` | aleo USDCx → ethereum USDC | `2.000001` USDCx private burn, delivers 1 atomic unit |
+
+The `metadata-required` mainnet routes (ALEO on ethereum/solana/base/hyperevm,
+USAD) are parametrized too and skip with `registry:metadata-required` —
+`tests/live/test_lifecycle_live.py` builds its parameters by enumerating
+`DEFAULT_REGISTRY.routes(...)`, so a route that no case covers raises at
+import rather than disappearing. The same two xReserve case functions run the
+testnet pair (`xreserve:sepolia/usdc->aleo-testnet/usdcx` at `3` USDC, then
+`xreserve:aleo-testnet/usdcx->sepolia/usdc` at `2.000001`); the testnet keys
+and RPC are `BRIDGE_LIVE_ALEO_TESTNET_PRIVATE_KEY`/`ALEO_E2E_PRIVATE_KEY`,
+`BRIDGE_LIVE_EVM_TESTNET_PRIVATE_KEY`/`EVM_PRIVATE_KEY`/`BRIDGE_EVM_PRIVATE_KEY`
+and `SEPOLIA_RPC_URL`/`BRIDGE_LIVE_SEPOLIA_RPC_URL` (public default when
+unset). Testnet needs no mainnet acknowledgement — the funds gate alone.
+
+**Recovery is not simulated.** Every funded test runs in two phases: the
+first quotes, prechecks and calls `execute` once, then returns; the client
+that executed is discarded, and a brand-new `Bridge` over the same
+`FileCheckpointStore` finishes the transfer from `bridge.pending()` →
+`bridge.recover(checkpoint)` → `wait`/`resume`/`complete`. `execute` is never
+called twice for one transfer, and `BRIDGE_LIVE_XRESERVE_AMOUNT` (or
+`BRIDGE_LIVE__AMOUNT`) overrides an amount.
+
+**Funding per run** (from the 2026-09-17 read-only mainnet quote sweep):
+ETH/WBTC/USDT Hyperlane deposits cost ≈0.0000838 ETH each in native Hyperlane
+fees plus L1 gas (USDT also needs one approval); the Aleo-origin legs cost
+8.174147 (ETH), 9.138947 (WBTC), 9.138947 (USDT) and 7.661056 (SOL) credits in
+IGP payment plus the Aleo transaction fee, and need the asset's public
+balance on Aleo; solana → aleo costs 5,647,521 lamports all-in; the xReserve
+deposit needs 2 USDC plus gas, and the burn needs an unspent private USDCx
+record of at least 2.000001. Return legs spend what the matching inbound leg
+minted, so run inbound first — an unfunded return leg skips with its
+shortfall printed rather than failing.
+
+**Running it.** *Quote only* — prices and prechecks every route and submits
+nothing, whatever is acknowledged:
+
+```sh
+python scripts/rehearse.py --case evm-hyperlane --quote-only
+python scripts/rehearse.py --case evm-xreserve --report run.json # submits only if acknowledged
+```
+
+*Testnet* (Sepolia ⇄ aleo-testnet, the funds gate only — no mainnet
+acknowledgement):
+
+```sh
+BRIDGE_LIVE_FUNDS=1 BRIDGE_LIVE_STATE_DIR="$HOME/.bridge-live" \
+ pytest -m live -s tests/live/test_lifecycle_live.py::test_testnet_evm_xreserve_deposit
+BRIDGE_LIVE_FUNDS=1 BRIDGE_LIVE_STATE_DIR="$HOME/.bridge-live" \
+ pytest -m live -s tests/live/test_lifecycle_live.py::test_testnet_aleo_xreserve_return
+```
+
+*Mainnet, quote only* — the acknowledgement that names the cases, and
+deliberately no execute variable, so each route is priced and prechecked and
+nothing is signed:
+
+```sh
+BRIDGE_LIVE_FUNDS=1 BRIDGE_LIVE_STATE_DIR="$HOME/.bridge-live" \
+ BRIDGE_LIVE_MAINNET_ACK= \
+ BRIDGE_LIVE_MAINNET_CASES=evm-hyperlane,evm-xreserve,aleo-hyperlane,aleo-xreserve,solana-hyperlane \
+ pytest -m live -s tests/live/test_lifecycle_live.py
+```
+
+*Mainnet, for real* — **you** type this, in your own shell, for one command;
+both acknowledgement values are the constants in `tests/live/config.py` and
+appear nowhere in this repository in a copy-pasteable form. Nothing in the
+suite or the CLI ever sets them:
+
+```sh
+BRIDGE_LIVE_FUNDS=1 BRIDGE_LIVE_STATE_DIR="$HOME/.bridge-live" \
+ BRIDGE_LIVE_MAINNET_ACK= \
+ BRIDGE_LIVE_MAINNET_CASES= \
+ BRIDGE_LIVE_MAINNET_EXECUTE= \
+ pytest -m live -s "tests/live/test_lifecycle_live.py::test_evm_xreserve"
+```
+
+*Resuming* an interrupted transfer (the run prints this line itself):
+
+```sh
+python scripts/rehearse.py --recover "$BRIDGE_LIVE_STATE_DIR/mainnet/-.json"
+```
+
+Exit codes: `0` ok, `1` a case failed, `2` something is still pending. The
+harness itself (route selection, state paths, the exit-code table) is
+covered hermetically by `tests/test_live_helpers.py`.
+
+## Environment variables
+
+| Variable | Used by | Meaning |
+| --- | --- | --- |
+| `BRIDGE_PRIVATE_KEY` | `from_env`, `from_profile` (import), MCP | Aleo private key (`APrivateKey1…`), **required** by `from_env` |
+| `ALEO_ENDPOINT` | `from_env` | node API origin (default `https://edge.provable.com/api`) |
+| `ALEO_NETWORK` | `from_env` | `mainnet` (default) or `testnet` |
+| `ALEO_API_KEY`, `ALEO_CONSUMER_ID` | `from_env` | optional Provable credentials for legacy endpoints |
+| `EVM_PRIVATE_KEY`, `ETHEREUM_RPC_URL` | `from_env`, rehearsal | Ethereum signer + RPC (both or neither); aliases `BRIDGE_EVM_PRIVATE_KEY` / `BRIDGE_LIVE_ETHEREUM_RPC_URL` — used by the user's live shell/veil config, and NOT live-test-only: ordinary `Ethereum.from_env()` reads them too, so leaving one exported points everyday calls at that endpoint; the primary variable wins when both are set |
+| `SOLANA_PRIVATE_KEY`, `SOLANA_RPC_URL` | `from_env`, rehearsal | Solana signer (base58 or `id.json` array) + RPC (optional); aliases `BRIDGE_SOLANA_PRIVATE_KEY` / `BRIDGE_LIVE_SOLANA_RPC_URL`, same precedence and same everyday-call caveat as the Ethereum pair |
+| `BRIDGE_CHECKPOINT_DIR` | `from_env` | bind a `FileCheckpointStore` |
+| `ALEO_BRIDGE_HOME` | `from_profile` | profile directory (default `~/.aleo-bridge/`), holds only the Aleo key, mode 600 |
+| `ALEO_E2E_PRIVATE_KEY` | live tests / rehearsal, testnet | testnet Aleo key (alias `BRIDGE_LIVE_ALEO_TESTNET_PRIVATE_KEY`) |
+| `BRIDGE_LIVE_FUNDS`, `BRIDGE_LIVE_STATE_DIR` | live tests, rehearsal | `1` + a directory outside the repo — funded cases exist at all |
+| `BRIDGE_LIVE_MAINNET_ACK`, `BRIDGE_LIVE_MAINNET_CASES` | live tests, rehearsal | `I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS` (see `tests/live/config.py`) + `leg-3,leg-5,…` — the named mainnet cases may run |
+| `BRIDGE_LIVE_MAINNET_EXECUTE` | live tests, rehearsal | `I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS` — without it every case quotes and prechecks only |
+
+Gates and acknowledgement strings live only in `tests/live/config.py` — nothing
+in this repository sets them, and they never appear here in copy-pasteable
+form.
+
+## Tests
+
+```sh
+cd bridge-sdk && .venv/bin/python -m pytest -q # hermetic
+.venv/bin/python -m pytest -q -m "not live" # same set, explicit marker
+BRIDGE_LIVE_READS=1 .venv/bin/python -m pytest -m live tests/live -q # read-only mainnet checks
+BRIDGE_LIVE_READS=1 BRIDGE_LIVE_SIMULATE=1 .venv/bin/python -m pytest -m live tests/live -q
+```
+
+Literals and vectors: `docs/veil-brief.md`.
+
+## Development
+
+```sh
+cd bridge-sdk && python -m venv .venv && .venv/bin/pip install -e '.[dev]'
+.venv/bin/python -m pytest -q # unit + mocked integration
+.venv/bin/python codegen/gen_context.py --check # AGENTS.md is generated from docstrings
+```
diff --git a/bridge-sdk/codegen/gen_context.py b/bridge-sdk/codegen/gen_context.py
new file mode 100644
index 0000000..90ab664
--- /dev/null
+++ b/bridge-sdk/codegen/gen_context.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+# bridge-sdk/codegen/gen_context.py
+"""Render AGENTS.md from the SDK's docstrings — the anti-drift context page.
+
+Tier 1 = the lifecycle verbs + the conversation pattern; Tier 2 = the protocol
+modules and the registry's route table. Run with no args to rewrite both
+copies of AGENTS.md; ``--check`` exits 1 when they are stale (CI); ``--stdout``
+prints instead of writing.
+"""
+from __future__ import annotations
+
+import argparse
+import inspect
+import sys
+from pathlib import Path
+
+_ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(_ROOT / "python"))
+
+from aleo_bridge import Bridge # noqa: E402
+from aleo_bridge.eth import EthModule # noqa: E402
+from aleo_bridge.freezelist import FreezeList # noqa: E402
+from aleo_bridge.hyperlane import HyperlaneModule # noqa: E402
+from aleo_bridge.registry import DEFAULT_REGISTRY # noqa: E402
+from aleo_bridge.sol import SolModule # noqa: E402
+from aleo_bridge.xreserve import XReserveModule # noqa: E402
+
+OUTS = [_ROOT / "AGENTS.md", _ROOT / "python" / "aleo_bridge" / "AGENTS.md"]
+
+TIER1 = ["from_env", "from_profile", "status", "quote", "execute", "wait", "recover", "resume",
+ "complete", "pending"]
+TIER2 = [
+ ("hyperlane.transfer_remote", HyperlaneModule.transfer_remote),
+ ("hyperlane.quote_gas_payment", HyperlaneModule.quote_gas_payment),
+ ("xreserve.burn", XReserveModule.burn),
+ ("xreserve.private_mint", XReserveModule.private_mint),
+ ("xreserve.get_attestation", XReserveModule.get_attestation),
+ ("shield", Bridge.shield),
+ ("unshield", Bridge.unshield),
+ ("freezelist.exclusion_proof", FreezeList.exclusion_proof),
+ ("eth.transfer_remote", EthModule.transfer_remote),
+ ("eth.deposit_usdc", EthModule.deposit_usdc),
+ ("eth.quote_transfer_remote", EthModule.quote_transfer_remote),
+ ("sol.transfer_remote", SolModule.transfer_remote),
+ ("sol.quote_transfer_remote", SolModule.quote_transfer_remote),
+]
+
+QUICKSTART = """\
+```python
+from aleo_bridge import Bridge
+
+bridge = Bridge.from_env() # BRIDGE_PRIVATE_KEY (+ EVM/Solana keys) from the environment
+print(bridge.status()) # addresses, balances, pending transfers
+quote = bridge.quote("ethereum/wbtc", "aleo/wbtc", amount="0.001", recipient=bridge.aleo_address())
+print(quote.fees, quote.amount_out) # show these to the user BEFORE executing
+progress = bridge.execute(quote.plan) # source step; checkpoints saved to the bound store
+progress = bridge.wait(progress) # stops at resume / complete / done / failed
+if progress.next == "resume": progress = bridge.wait(bridge.resume(progress))
+if progress.next == "complete": progress = bridge.wait(bridge.complete(progress, secret_nonce=nonce))
+assert progress.next == "done", progress.error
+```"""
+
+CONVERSATION_PATTERN = """\
+## Serving a chatting user (the conversation pattern)
+
+### Keys and identity
+
+1. **NEVER ask the user to paste a private key into the conversation.** Keys
+ come from the environment only: `BRIDGE_PRIVATE_KEY` (Aleo),
+ `EVM_PRIVATE_KEY` + `ETHEREUM_RPC_URL`, `SOLANA_PRIVATE_KEY` (+ optional
+ `SOLANA_RPC_URL`), set in the user's own shell before the process starts.
+ `Bridge.from_profile()` creates an Aleo key on first use and never writes
+ EVM/Solana keys to disk.
+2. `status()` first in any session: which chains are configured, balances of
+ every bridge asset, and the pending transfers in the checkpoint store. A
+ pending transfer is finished with `recover` → `wait`/`resume`/`complete`,
+ never by starting a new one.
+
+### Quote first, always
+
+3. **Always `quote` before `execute`** and show the user the route, the fees
+ and `amount_out` in human units with symbols ("2 USDC → 2 USDCx; Hyperlane
+ hook payment 8.17 ALEO"), never raw atomic units. Minimums: xReserve
+ needs at least 2 USDC in and strictly more than the 2 USDCx withdrawal fee
+ out; Hyperlane moves one atomic unit but network fees and the relayer
+ payment cost more than that — say so.
+4. Only `execute` after the user confirms. Through the agent tools every
+ write requires `confirm=true`; without it the tool returns the quote and
+ moves nothing. A live mainnet execution additionally needs the user's
+ own `BRIDGE_LIVE_MAINNET_EXECUTE` acknowledgement — never set it yourself;
+ without it, treat any mainnet run as a rehearsal.
+
+### The source step is irreversible
+
+5. Once the deposit / dispatch / burn is broadcast the funds are committed.
+ A timeout, an RPC error or a crash after that point is an UNKNOWN outcome,
+ not a failure: recover from the last checkpoint (`recover(checkpoint)` or
+ `pending()`) — never run `execute` again for the same transfer. This is
+ the funds-safety rule above all others: never resend after an ambiguous
+ broadcast.
+
+### What `progress.next` means for the user
+
+| `progress.next` | Status | Tell the user | Do |
+| --- | --- | --- | --- |
+| `wait` | source confirming, attestation pending, delivery pending | "In flight; I'll keep checking." | `wait(progress)` (or re-check later from the checkpoint) |
+| `resume` | `SOURCE_SUBMISSION_PENDING` | "An approval confirmed / a proof was built but the transfer itself was not submitted; I can submit it now." | confirm, then `resume(progress)` |
+| `complete` | `DESTINATION_ACTION_REQUIRED` | "Circle attested your deposit; your private mint needs your signature (and the secret nonce)." | confirm, then `complete(progress, secret_nonce=...)` |
+| `done` | `COMPLETED` | "Delivered." Report source and destination transaction ids. | nothing |
+| `failed` | `FAILED` / `EXPIRED` | Relay `progress.error`; the source step did not commit funds or was rejected. | nothing — a new transfer needs a new quote |
+
+`wait` raising `PollingTimeoutError` is NOT a failure — say the transfer is
+still in flight and check again later.
+
+### Private mints and the secret nonce
+
+6. `mint_mode="private"` (USDC → USDCx) commits `(recipient, secret_nonce)` on
+ Ethereum. The same `secret_nonce` is required by `complete`; the SDK
+ **never stores** it and checkpoints exclude it (and every other secret).
+ Tell the user to keep it (the default `0scalar` needs no storage but adds
+ no entropy). Only the recipient's Aleo key can complete a private mint —
+ make sure the recipient IS the configured Aleo address before depositing.
+7. Aleo-origin Hyperlane transfers spend PUBLIC balances: `unshield` a private
+ record first. Hyperlane delivers into public balances; `shield` afterwards
+ if the user wants privacy. Private xReserve burns spend records directly.
+
+### While acting
+
+8. Writes are slow (proving + confirmation ≈ a minute or two on Aleo; Circle
+ attestation and Hyperlane relay take minutes). Never re-submit because a
+ call seems slow — `status()` / `recover` first.
+9. Confirm, act, report ids. Errors name their own fix — read the exception
+ message and do what it says.
+"""
+
+
+def _entry(name: str, fn: object) -> str:
+ try:
+ sig = str(inspect.signature(fn)) # type: ignore[arg-type]
+ except (TypeError, ValueError):
+ sig = "(...)"
+ doc = inspect.getdoc(fn) or ""
+ return f"### `{name}{sig}`\n\n{doc.strip()}\n"
+
+
+def _route_table() -> list[str]:
+ rows = ["| Route id | Protocol | Environment | Availability |", "| --- | --- | --- | --- |"]
+ for route in DEFAULT_REGISTRY.routes(include_unavailable=True, environment=None):
+ rows.append(f"| `{route.id}` | {route.protocol} | {route.environment} | {route.availability} |")
+ return rows
+
+
+def render() -> str:
+ parts = [
+ "# aleo-bridge — agent guide",
+ "",
+ "> GENERATED from SDK docstrings by `codegen/gen_context.py` — do not",
+ "> edit by hand; edit the docstrings and regenerate.",
+ "",
+ "Typed Python client that moves assets between Aleo, Ethereum and Solana",
+ "over the reviewed Hyperlane warp routes and Circle xReserve deployments",
+ "(`pip install aleo-bridge-sdk[evm,solana]`, imports as `aleo_bridge`).",
+ "MCP alternative: `python -m aleo_bridge.mcp` exposes the same lifecycle as",
+ "tools; `aleo_bridge.agent.bridge_tools()` gives Claude-shape tool schemas.",
+ f"Registry version `{DEFAULT_REGISTRY.version}`.",
+ "",
+ "## Tier 1 — the lifecycle (quote → execute → wait, then resume / complete as asked)",
+ "",
+ QUICKSTART,
+ "",
+ ]
+ parts += [_entry(n, getattr(Bridge, n)) for n in TIER1]
+ parts += [
+ CONVERSATION_PATTERN,
+ "",
+ "## Tier 2 — the protocol modules (building your own flows)",
+ "",
+ "Every Aleo write returns an `AleoCall`: nothing touches the network until",
+ "`.simulate()` (free), `.prove()` / `.delegate_prepared()` (proved, not",
+ "broadcast — checkpoint it), `.submit_prepared()`, `.transact()` (local",
+ "proving + broadcast) or `.delegate()` (DPS + broadcast). EVM and Solana",
+ "writes return `EvmCall` / `SolCall` with `.build()` (unsigned) and `.send()`.",
+ "The lifecycle verbs above compose these; use them directly only when you",
+ "need a single leg. Confirm-gated writes and the never-resend rule above",
+ "apply here too — these are the same broadcasts, just one leg at a time.",
+ "",
+ ]
+ parts += [_entry(name, fn) for name, fn in TIER2]
+ parts += ["### Routes in the pinned registry", ""]
+ parts += _route_table()
+ parts += ["", "`metadata-required` routes are listed but refused by `quote`/`execute`",
+ "until their deployments are reviewed upstream.", ""]
+ return "\n".join(parts) + "\n"
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--check", action="store_true", help="exit 1 when AGENTS.md is stale (CI gate)")
+ ap.add_argument("--stdout", action="store_true")
+ args = ap.parse_args()
+ page = render()
+ if args.stdout:
+ print(page, end="")
+ return 0
+ if args.check:
+ for out in OUTS:
+ current = out.read_text() if out.exists() else ""
+ if current != page:
+ print(f"{out} is stale — run: python codegen/gen_context.py", file=sys.stderr)
+ return 1
+ return 0
+ for out in OUTS:
+ out.write_text(page)
+ print(f"wrote {out} ({len(page)} chars)")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/bridge-sdk/docs/veil-brief.md b/bridge-sdk/docs/veil-brief.md
new file mode 100644
index 0000000..9924f6f
--- /dev/null
+++ b/bridge-sdk/docs/veil-brief.md
@@ -0,0 +1,326 @@
+# Veil Bridge (`@provablehq/aleo-bridge-sdk` v0.1.0) — Capability Port Brief
+
+Source: `/Users/privacydaddy/dev/aleo-viem/packages/bridge`. Deps: viem ^2.21, bs58 ^6, @provablehq/veil-core. Optional peers: @provablehq/sdk (private xReserve mints + program-address derivation), @solana/kit ^8.
+
+## 1. REGISTRY
+
+### 1.1 Data model (`src/types/protocol.ts`)
+```
+BridgeProtocol = 'xreserve' | 'hyperlane'
+BridgeEnvironment = 'mainnet' | 'testnet'
+BridgeChainFamily = 'aleo' | 'evm' | 'solana'
+BridgeAssetKind = 'native' | 'token'
+BridgeRouteAvailability = 'active' | 'metadata-required' | 'disabled'
+AleoPrivacyKind = 'arc20' | 'arc22'
+AleoMintMode = 'public' | 'record' | 'private'
+BridgeStepExecutor = 'aleo-wallet' | 'evm-wallet' | 'solana-wallet' | 'protocol'
+BridgeExecutionStepKind = approve|deposit|burn|dispatch|wait-attestation|mint|withdraw|wait-delivery|confirm-delivery
+
+ProtocolBridgeChain { id, displayName, family, environment, nativeCurrencySymbol, protocolDomains?: {xreserve?, hyperlane?} }
+BridgeAssetLocator { kind: 'aleo-program'|'evm-contract'|'solana-mint'|'native', value, tokenId? }
+AleoPrivacyCapability{ kind: 'arc20'|'arc22', program }
+ProtocolBridgeAsset { id, key, chainId, symbol, name, decimals, kind, locator?, addressValidationRegex?, privacy? }
+ProtocolBridgeRoute { id, protocol, environment, sourceAssetId, destinationAssetId, availability, deploymentId?, source?, metadata? }
+BridgeRegistry { version, chains[], assets[], routes[], sources[], getAssets(), getRoutes() }
+```
+getAssets({environment?, chainId?, symbol?}) case-insensitive. getRoutes({environment?, protocol?, sourceChainId?, destinationChainId?, symbol?, includeUnavailable?}) excludes disabled unless includeUnavailable; metadata-required always visible; symbol matches source OR destination.
+
+Registry version: `'2026-08-31.solana-deposits.1'`.
+```
+EVM_ADDRESS = '^0x[0-9a-fA-F]{40}$'
+SOLANA_ADDRESS = '^[1-9A-HJ-NP-Za-km-z]{32,44}$'
+ALEO_ADDRESS = '^aleo1[0-9a-z]{58}$'
+```
+
+### 1.2 Validation
+Unique chain/asset/route ids; asset chainId exists; scoped key `${chainId}/${key}` unique; decimals int>=0; regex compiles; privacy only on aleo family. Route source/dest assets exist; both chains' environment == route.environment. Solana gate: active hyperlane route with solana source requires warpProgramAddress, tokenPda, nativeCollateralPda, dispatchAuthorityPda, mailboxProgramAddress, mailboxOutboxPda, igpProgramAddress, igpProgramDataPda, igpAccount, splNoopProgramAddress, destinationDomain (number), destinationGasAmount, registryCommit, solanaReviewedAt, solanaConfigSource. igpOverheadAccount optional.
+
+### 1.3 Chains
+| id | family | env | native | protocolDomains |
+|---|---|---|---|---|
+| aleo | aleo | mainnet | ALEO | xreserve 10002, hyperlane 1634493807 |
+| ethereum | evm | mainnet | ETH | xreserve 0, hyperlane 1 |
+| solana | solana | mainnet | SOL | hyperlane 1399811149 |
+| base | evm | mainnet | ETH | — |
+| hyperevm | evm | mainnet | HYPE | — |
+| aleo-testnet | aleo | testnet | ALEO | xreserve 10002, hyperlane 1617853565 |
+| sepolia | evm | testnet | ETH | hyperlane 11155111 |
+
+### 1.4 Assets
+| id | symbol | dec | kind | locator | tokenId | privacy |
+|---|---|---|---|---|---|---|
+| aleo/aleo | ALEO | 6 | native | aleo-program credits.aleo | | |
+| aleo/usdcx | USDCx | 6 | token | aleo-program usdcx_stablecoin.aleo | | arc22 usdcx_stablecoin.aleo |
+| aleo/eth | ETH | 18 | token | aleo-program hyp_warp_token_eth_v2.aleo | aleo1t7f29tq9qng2lfvrkpcuvu59jn24hrmzqdyqfn6p0u5p80npfvqqecmkj8 | arc20 arc20_eth.aleo |
+| aleo/wbtc | WBTC | 8 | token | aleo-program hyp_warp_token_wbtc_v2.aleo | aleo1240fsvz2dhmj0cdtt8mc0yc8um9fmu236rqcl2qnlj9703hd2vpsdwyrtf | arc20 arc20_wbtc.aleo |
+| aleo/usdt | USDT | 6 | token | aleo-program hyp_warp_token_usdt_v2.aleo | aleo18yynfz0lrfx0tund540vy2z7gju7ekgqsueg5jgu28mpm2z42ufq7qua8y | arc20 arc20_usdt.aleo |
+| aleo/sol | SOL | 9 | token | aleo-program hyp_warp_token_sol_v2.aleo | aleo1aa0zt0vg9uwknekpqeefkvad55swp7833wc5crp2prv0lm4djuxs5r7k6v | arc20 arc20_sol.aleo |
+| aleo/usad | USAD | 6 | token | aleo-program usad_stablecoin.aleo | | |
+| ethereum/usdc | USDC | 6 | token | evm 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | |
+| ethereum/eth | ETH | 18 | native | native ETH | | |
+| ethereum/wbtc | WBTC | 8 | token | evm 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 | | |
+| ethereum/usdt | USDT | 6 | token | evm 0xdAC17F958D2ee523a2206206994597C13D831ec7 | | |
+| ethereum/aleo | ALEO | 6 | token | (none) | | |
+| ethereum/usad | USAD | 6 | token | (none) | | |
+| solana/sol | SOL | 9 | native | native SOL | | |
+| solana/aleo, base/aleo, hyperevm/aleo | ALEO | 6 | token | (none) | | |
+| aleo-testnet/usdcx | USDCx | 6 | token | aleo-program test_usdcx_stablecoin.aleo | | arc22 test_usdcx_stablecoin.aleo |
+| sepolia/usdc | USDC | 6 | token | evm 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 | | |
+
+### 1.5 Routes (22 total). Route id = `${protocol}:${sourceAssetId}->${destinationAssetId}`
+```
+XRESERVE_SOURCE = 'https://developers.circle.com/xreserve/references/supported-blockchains-and-domains'
+HYPERLANE_REGISTRY_COMMIT = '2621c16f2db1ccb46643265c110dac5ca2c7c51a'
+HYPERLANE_SOURCE = 'https://github.com/hyperlane-xyz/hyperlane-registry/tree/2621c16f2db1ccb46643265c110dac5ca2c7c51a/deployments/warp_routes'
+```
+
+**A. xReserve mainnet** `xreserve:ethereum/usdc->aleo/usdcx` + reverse, active, deploymentId 'xreserve-usdcx-aleo':
+```
+xReserveContract 0x8888888199b2Df864bf678259607d6D5EBb4e3Ce ; sourceChainId 1 ; sourceDomain 0
+ethereumDestinationDomain 0 ; arcDestinationDomain 26 ; remoteDomain 10002
+remoteToken usdcx_stablecoin.aleo
+remoteTokenBytes32 0x11ea7dab1d29d5f61500582c63e98c42e1165f9ba050ea9d0c6af9f871987711
+minimumAmountAtomic '2000000' ; withdrawalFeeAtomic '2000000' ; maxFeeAtomic '100000'
+bridgeProgram usdcx_bridge_v2.aleo ; wrapperProgram shielded_usdcx_wrapper.aleo
+attestationBaseUrl https://xreserve-api.circle.com/v1/attestations
+```
+**B. xReserve testnet** `xreserve:sepolia/usdc->aleo-testnet/usdcx` + reverse, active, 'xreserve-usdcx-aleo-testnet':
+```
+xReserveContract 0x008888878f94C0d87defdf0B07f46B93C1934442 ; sourceChainId 11155111 ; sourceDomain 0
+ethereumDestinationDomain 0 ; arcDestinationDomain 26 ; remoteDomain 10002
+remoteToken test_usdcx_stablecoin.aleo
+remoteTokenBytes32 0xb143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401
+minimumAmountAtomic '2000000' ; withdrawalFeeAtomic '2000000' ; maxFeeAtomic '100000'
+bridgeProgram test_usdcx_bridge_v2.aleo ; wrapperProgram shielded_usdcx_wrapper.aleo
+attestationBaseUrl https://xreserve-api-testnet.circle.com/v1/attestations
+```
+**ETHEREUM_HYPERLANE_COMMON** (every Ethereum<->Aleo Hyperlane route):
+```
+sourceChainId 1 ; destinationDomain 1634493807
+mailboxAddress 0xc005dc82818d67AF737725bD4bf75435d065D239
+interchainGasPaymaster 0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611
+interchainSecurityModule 0x0000000000000000000000000000000000000000
+registryCommit 2621c16f2db1ccb46643265c110dac5ca2c7c51a
+```
+**ALEO_MAILBOX_METADATA** (every Hyperlane route):
+```
+aleoHookManagerProgram hyp_hook_manager.aleo ; aleoMailboxProgram hyp_mailbox.aleo ; aleoMailboxProgramEdition 0
+aleoMailboxLocalDomain 1634493807 ; aleoMailboxObservedNonce 170 ; aleoMailboxObservedProcessCount 291
+aleoMailboxDefaultIsm aleo1yvf5kcsdgnescqq2lar83mms79yh3ugvc3y0mdnlgvx4lyh5zugqr9hptk
+aleoMailboxDefaultHook aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74 (= IGP identity in gas-config key)
+aleoMailboxRequiredHook aleo1yxevh9qgxehej46j7vueplwjcpfdfml2dje3ey4ukzknx7wzasgqnxgq82
+aleoMailboxDispatchProxy aleo1sge9kmjzs3d8fqrscy4hwn7vf9vw4jcxe877lv0m2w8hay78lsxsqg975s
+aleoMailboxOwner aleo1ypf8xgvz560ukw25hufj3d77gx69pdcy70nssdfdxd97j80d7cqs98d7x8
+aleoMailboxMetadataReviewedAt 2026-08-17 ; aleoMailboxStateVerified true
+```
+**C. hyperlane:ethereum/eth->aleo/eth** active 'ETH/aleo': COMMON + routerAddress 0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A, routerType native, destinationRouter hyp_warp_token_eth_v2.aleo/aleo1t7f29tq9qng2lfvrkpcuvu59jn24hrmzqdyqfn6p0u5p80npfvqqecmkj8 + MAILBOX.
+**D. hyperlane:aleo/eth->ethereum/eth** active:
+```
+aleoRouterProgram hyp_warp_token_eth_v2.aleo ; aleoDestinationDomain 1 ; aleoProgramEdition 0 ; aleoTokenType '1'
+aleoTokenOwner aleo1wq6f6qdqya44avznygz5hae40u3mjg64w0r93a4qfu4utpf8cg9q566f4r
+aleoIsm = aleoHook = aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc
+aleoTokenId 133188123661477349522757068766864658505569365361420630212878794317749195359field
+aleoLocalDecimals 18 ; aleoRemoteDecimals 18
+aleoRemoteRouterEvmAddress 0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A
+aleoRemoteRouterRecipient [0u8 x12, 56u8, 212u8, 71u8, 105u8, 79u8, 92u8, 31u8, 119u8, 58u8, 227u8, 19u8, 44u8, 249u8, 59u8, 243u8, 11u8, 126u8, 193u8, 250u8, 90u8]
+aleoRemoteRouterGas '44000'
+aleoAllowanceSpender0 aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74 ; Spender1..3 = aleo1qqq…3ljyzc ; Amount0..3 '0'
+aleoRecipient '[0u128, 0u128]' (placeholder) ; aleoPlaceholderConfiguration false ; aleoWithdrawalReviewedAt 2026-08-26
+```
+**E. hyperlane:ethereum/wbtc->aleo/wbtc** active 'WBTC/aleo': COMMON + routerAddress 0x20CDC85778b732073F7EecEF3DF25c0d310f8772, routerType collateral, tokenAddress 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, destinationRouter hyp_warp_token_wbtc_v2.aleo/aleo1240fsvz2dhmj0cdtt8mc0yc8um9fmu236rqcl2qnlj9703hd2vpsdwyrtf + MAILBOX.
+**F. hyperlane:aleo/wbtc->ethereum/wbtc** active:
+```
+aleoRouterProgram hyp_warp_token_wbtc_v2.aleo ; aleoDestinationDomain 1 ; aleoProgramEdition 0 ; aleoTokenType '1'
+aleoTokenOwner aleo14jauje2a5sncm9u5t3mt6qqv3eq2hatkddskccs0dvsy35a0x58q0d6f95
+aleoTokenId 1505227928464760254508513036497943623956572091841806589002910775534260084309field
+aleoLocalDecimals 8 ; aleoRemoteDecimals 8
+aleoRemoteRouterEvmAddress 0x20CDC85778b732073F7EecEF3DF25c0d310f8772
+aleoRemoteRouterRecipient [0u8 x12, 32u8, 205u8, 200u8, 87u8, 120u8, 183u8, 50u8, 7u8, 63u8, 126u8, 236u8, 239u8, 61u8, 242u8, 92u8, 13u8, 49u8, 15u8, 135u8, 114u8]
+aleoRemoteRouterGas '68000'
+```
+**G. hyperlane:ethereum/usdt->aleo/usdt** active 'USDT/aleo': COMMON + routerAddress 0x3C2064D78e4578E8F936E3db42aEF044E33FBF31, routerType collateral, tokenAddress 0xdAC17F958D2ee523a2206206994597C13D831ec7, destinationRouter hyp_warp_token_usdt_v2.aleo/aleo18yynfz0lrfx0tund540vy2z7gju7ekgqsueg5jgu28mpm2z42ufq7qua8y, **requiresApprovalReset true** + MAILBOX.
+**H. hyperlane:aleo/usdt->ethereum/usdt** active:
+```
+aleoRouterProgram hyp_warp_token_usdt_v2.aleo ; aleoDestinationDomain 1 ; aleoProgramEdition 1 ; aleoTokenType '1'
+aleoTokenOwner aleo1l3gwacmjruxryy9c7c4fn0acyzprf29hucrvthw7f63lpyhd5y9srydq8z
+aleoTokenId 8295938150000417034830036849466229528602563851235385582732969109393809606969field
+aleoLocalDecimals 6 ; aleoRemoteDecimals 18 ; aleoScale '1000000000000'
+aleoRemoteRouterEvmAddress 0x3C2064D78e4578E8F936E3db42aEF044E33FBF31
+aleoRemoteRouterRecipient [0u8 x12, 60u8, 32u8, 100u8, 215u8, 142u8, 69u8, 120u8, 232u8, 249u8, 54u8, 227u8, 219u8, 66u8, 174u8, 240u8, 68u8, 227u8, 63u8, 191u8, 49u8]
+aleoRemoteRouterGas '68000'
+```
+**I. hyperlane:solana/sol->aleo/sol** active 'SOL/aleo':
+```
+warpProgramAddress 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7
+tokenPda JDkpV5CsSbhyGhHhirC5DjGPTcuKWUVHtBZ5MFsgu3ZW
+nativeCollateralPda 8HY3hxmnrWwqEmcdwkSnfN9wEQFUkyiwZvU1vMbnXgbC
+dispatchAuthorityPda ATDttjggAZKyS19kcV6Rn56oMi49gDprZGckRou9vkkY
+mailboxProgramAddress E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi
+mailboxOutboxPda BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk
+igpProgramAddress BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv
+igpProgramDataPda 8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho
+igpAccount JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M
+igpOverheadAccount AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF
+splNoopProgramAddress noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV
+destinationDomain 1634493807 ; destinationGasAmount '464000'
+registryCommit 418056e21734d26a7d14692e0ec5e902cc9e86bf ; solanaReviewedAt 2026-08-31
+```
+**J. hyperlane:aleo/sol->solana/sol** active:
+```
+aleoRouterProgram hyp_warp_token_sol_v2.aleo ; aleoDestinationDomain 1399811149 ; aleoTokenType '1'
+aleoTokenOwner aleo1wr8rfr4ggedjxtg5e23s38zqkgy2j05uc9l8t4akjp5zcw3levpswkwk45
+aleoTokenId 6148061383892805373029428966764338809222769879628268522058032128225601478383field
+aleoLocalDecimals 9 ; aleoRemoteDecimals 9
+aleoRemoteRouterSolanaAddress 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7
+aleoRemoteRouterRecipient [112u8, 4u8, 72u8, 22u8, 219u8, 143u8, 68u8, 202u8, 21u8, 197u8, 236u8, 182u8, 198u8, 142u8, 52u8, 96u8, 142u8, 38u8, 51u8, 113u8, 116u8, 143u8, 96u8, 123u8, 104u8, 126u8, 97u8, 73u8, 7u8, 6u8, 211u8, 122u8]
+aleoRemoteRouterGas '300000'
+```
+**K–N.** metadata-required ALEO pairs ('ALEO/aleo'): aleo/aleo<->ethereum/aleo, <->solana/aleo, <->base/aleo, <->hyperevm/aleo (MAILBOX only).
+**O/P.** hyperlane:ethereum/usad->aleo/usad and reverse: metadata-required ('USAD/aleo'); reverse uses placeholders (aleoPlaceholderConfiguration true) — execution refuses.
+Placeholder scaffold: ALEO_PLACEHOLDER_ADDRESS aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n; aleoTokenType '0'; aleoTokenId '0field'; recipient zeros; gas '0'.
+Testnet registry = only the two Sepolia<->aleo-testnet xReserve routes. No testnet Hyperlane route.
+
+## 2. LIFECYCLE ACTIONS
+
+### 2.1 prepare(registry, params) -> BridgePlan (pure)
+Inputs {source:{chain,asset}, destination:{chain,asset}, bridgeProtocol?, amount, recipient, sender?, mintMode?}. Filters routes on exact asset pair, not disabled, optional protocol; 0 or >1 matches → error. mintMode default 'public'; non-public only for xreserve with aleo destination. Amount parsed with source decimals (>0) and re-parsed with destination decimals (precision). Recipient regex-checked. fees: [].
+Steps: xReserve EVM→Aleo: source-approval(reversible) → source-deposit(IRREVERSIBLE) → deposit-attestation(protocol) → destination-mint(aleo-wallet iff private else protocol). xReserve Aleo→EVM: source-burn(IRREVERSIBLE) → withdrawal-attestation → destination-withdrawal → destination-confirmation. Hyperlane: optional source-approval (token on non-aleo source) → source-dispatch(IRREVERSIBLE) → message-delivery → destination-confirmation.
+
+### 2.2 quote → kinds evm-hyperlane | solana-hyperlane | aleo-hyperlane | evm-xreserve | aleo-xreserve. aleo-xreserve quote makes NO network call: amountOut = amount − withdrawalFee (source decimals); fee entry {kind:'protocol', estimated:false}; throws if amount <= fee.
+
+### 2.3 execute params {plan, pollingIntervalMs?, confirmationTimeoutMs?, onCheckpoint?, mode?, userRecord?, merkleProof?, privateFee?, gasPaymentMicrocredits?, privateMintSecretNonce?, onProgress?}. Aleo Hyperlane mode 'caller'|'signer'; Aleo xReserve mode 'private'|'public'|'public-as-signer'. Two checkpoint hooks: post-proving pre-broadcast (status SOURCE_SUBMISSION_PENDING, protocolState.preparedTransaction = serialized tx) and post-broadcast. Aleo-Hyperlane reads destination balance BEFORE submitting (destinationBalanceBeforeAtomic, expectedDestinationIncreaseAtomic) and re-quotes IGP right before proving unless pinned (hook asserts exact equality).
+
+### 2.4 getStatus({plan, receipt}) — guards receipt.protocol == plan.protocol && protocolState.routeId == plan.route.id; terminal (COMPLETED|FAILED|EXPIRED) returned untouched. Branches:
+1. SOURCE_APPROVAL_PENDING+evm → tx receipt; reverted → FAILED; success → SOURCE_SUBMISSION_PENDING.
+2. SOURCE_CONFIRMING+aleo → transactionStatus; accepted → DELIVERY_PENDING; rejected → FAILED.
+3/4. SOURCE_CONFIRMING+hyperlane evm/solana → protocol getSourceStatus (extract messageId).
+5. DELIVERY_PENDING+hyperlane+messageId+dest aleo|evm → readHyperlaneDelivery on dest Mailbox → COMPLETED.
+6. DELIVERY_PENDING+hyperlane+source aleo → balance-diff fallback (current >= before + expected → COMPLETED).
+7. other hyperlane → unchanged. 8. xReserve Aleo→EVM DELIVERY_PENDING → unchanged. 9. else → 'Status refresh is not implemented'.
+10. xReserve EVM→Aleo: for ATTESTATION_PENDING|DELIVERY_PENDING|DESTINATION_ACTION_REQUIRED first read Aleo nullifier → COMPLETED if delivered. SOURCE_CONFIRMING → source status; ATTESTATION_PENDING → Circle getAttestation (pending → unchanged; complete+non-private → DELIVERY_PENDING + attestation; complete+private → DESTINATION_ACTION_REQUIRED + nextAction {kind:'xreserve-private-mint'}); DESTINATION_CONFIRMING → Aleo tx status → COMPLETED/FAILED.
+
+### 2.5 BridgeProgress.next
+```
+SOURCE_SUBMISSION_PENDING -> 'resume'
+DESTINATION_ACTION_REQUIRED -> 'complete'
+COMPLETED -> 'done'
+FAILED | EXPIRED -> 'failed'
+else -> 'wait'
+```
+Statuses: PREPARED, SOURCE_APPROVAL_PENDING, SOURCE_SUBMISSION_PENDING, SOURCE_CONFIRMING, ATTESTATION_PENDING, DESTINATION_ACTION_REQUIRED, DELIVERY_PENDING, DESTINATION_CONFIRMING, COMPLETED, FAILED, EXPIRED.
+
+### 2.6 wait({progress, until?, pollingIntervalMs=15000, timeoutMs=1200000, onUpdate?}) — always stops at SOURCE_SUBMISSION_PENDING, DESTINATION_ACTION_REQUIRED, COMPLETED, FAILED, EXPIRED ∪ until. Timeout → 'Bridge status polling timed out in state X'.
+
+### 2.7 recover({checkpoint}) — version must be 1; re-runs prepare(checkpoint.intent); checks route id and registryVersion. Aleo source: prepared-but-unbroadcast → next 'resume' with preparedTransaction; else requires transactionId → one getStatus from SOURCE_CONFIRMING. Solana: requires transactionId; validates (blockhash,lastValidBlockHeight) both-or-neither. EVM Hyperlane: recoverSourceCheckpoint (log scan). EVM xReserve: recoverSourceCheckpoint; submitted destination tx → DESTINATION_CONFIRMING; prepared destination tx preserved only if still DESTINATION_ACTION_REQUIRED.
+
+### 2.8 resume({progress, ...}) — requires next=='resume' and SOURCE_SUBMISSION_PENDING. Aleo: rebroadcast identical bytes; DuplicateTransaction = success. EVM: protocol execute with resume receipt (re-scans history before new dispatch).
+
+### 2.9 complete — xReserve private mint only; requires DESTINATION_ACTION_REQUIRED + nextAction xreserve-private-mint; re-validates payload/messageHash/attestation; rebroadcasts preparedDestinationTransaction if present else calls private_mint.
+
+### 2.10 createBridgeCheckpoint(plan, receipt) — ALLOWLIST (version 1):
+```
+intent: { source, destination, bridgeProtocol, amount, recipient, sender?, mintMode? (only aleo-program dest) }
+route: { id, registryVersion }
+source?: { approvalTransactionIds?, transactionId?, hookData?, blockhash?, lastValidBlockHeight?, preparedTransaction?: {transactionId, serializedTransaction} }
+destination?: { transactionId?, preparedTransaction? }
+deliveryVerification?: { balanceBeforeAtomic, expectedIncreaseAtomic }
+```
+EXCLUDED: private keys, record plaintext, secretNonce, attestation bodies, amountAtomic, maxFeeAtomic, remoteRecipientBytes32, messageHash, payload, nonce.
+
+### 2.11 shield / unshield — asset must be aleo family with privacy. amount → `${atomic}u128` (>0).
+shield: ARC-22 → transfer_public_to_private(recipient, amount); ARC-20 → shield(amount).
+unshield: ARC-22 → transfer_private_to_public(recipient, amount, record, merkleProof ?? EMPTY); ARC-20 → unshield(record, amount).
+EMPTY_MERKLE_PROOF_PAIR = `[{ siblings: [0field x16], leaf_index: 1u32 }, { siblings: [0field x16], leaf_index: 1u32 }]` (16 siblings each, comma+space joined).
+
+### 2.12 Errors — single `BridgeError(message, {cause})`. No code taxonomy.
+
+## 3. PROTOCOL MECHANICS
+
+### 3.1 EVM Hyperlane deposit
+ABI: quoteTransferRemote(uint32,bytes32,uint256) view returns ((address token,uint256 amount)[]); transferRemote(uint32,bytes32,uint256) payable returns (bytes32); event SentTransferRemote(uint32 indexed destination, bytes32 indexed recipient, uint256 amount); allowance/approve; event DispatchId(bytes32 indexed messageId).
+Quote: assert eth_chainId == sourceChainId. recipientBytes32 = aleoAddressToBytes32(recipient). Call quoteTransferRemote; nativeValueAtomic = Σ quotes with token == 0x0. Native route: nativeValue >= amount; fee = nativeValue − amount; msg.value = nativeValue (carries ETH + fee). Collateral: tokenAmount = Σ quotes with token == tokenAddress (>= amount); fee = nativeValue; msg.value = nativeValue.
+Execute: defaults poll 1000ms / timeout 120000ms. Fresh quote at last moment. Wallet addr must equal plan.sender. Approval (collateral): allowance < required → if allowance > 0 && requiresApprovalReset: approve(router, 0) first; then approve(router, required). Checkpoint each hash BEFORE polling. Dispatch: transferRemote(destinationDomain, recipientBytes32, amountAtomic) value=nativeValue to router. Checkpoint at SOURCE_CONFIRMING before polling. On success, messageId from DispatchId log → DELIVERY_PENDING, id = messageId ?? txHash. protocolState {routeId, approvalTxIds, sourceSender, recipientBytes32, destinationDomain, nativeValueAtomic, amountAtomic}.
+Recovery: getLogs(router, fromBlock=highest approval block) decode SentTransferRemote matching destination/recipient/amount; tx from == sender && to == router; >1 match → error.
+
+### 3.2 EVM xReserve USDC deposit
+ABI: depositToRemote(uint256 value, uint32 remoteDomain, bytes32 remoteRecipient, address localToken, uint256 maxFee, bytes hookData); event DepositedToRemote(address indexed localToken, uint256 value, address indexed localDepositor, bytes32 indexed remoteRecipient, uint32 remoteDomain, bytes32 remoteToken, uint256 maxFee, bytes hookData).
+Quote: amount >= minimumAmountAtomic; hookData; remoteRecipientBytes32 = public/record → aleoAddressToBytes32(recipient); PRIVATE → aleoAddressToBytes32(programAddress(wrapperProgram)). balanceOf + allowance; approvalRequired = allowance < amount.
+Execute: approve(xReserve, amount) if needed; depositToRemote(amount, remoteDomain, remoteRecipientBytes32, tokenAddress, maxFeeAtomic, hookData) — NO msg.value. Confirm: find DepositedToRemote log from xReserve contract, re-verify every field. logIndex → nonce = calculateXReserveDepositNonce(sourceDomain, txHash, logIndex); payload = buildXReserveDepositPayload(...); messageHash = keccak256(payload). Receipt id = messageHash, ATTESTATION_PENDING, protocolState += {sourceDomain, remoteDomain, depositLogIndex, nonce, payload, messageHash, bridgeProgram, wrapperProgram}.
+Attestation: GET {attestationBaseUrl}/{messageHash}; 404 → pending; other non-ok → error. Body {attestation:{payload, messageHash, attestation}}; verify echoed hash and keccak256(payload) == messageHash.
+Hook byte 0: 0 public, 1 record, 2 private.
+
+### 3.3 Aleo Hyperlane transfer_remote
+Program = aleoRouterProgram; fn 'transfer_remote' (mode caller) | 'transfer_remote_as_signer' (mode signer). 7 inputs:
+```
+0 "{ token_type: {T}u8, token_owner: {owner}, ism: {ism}, hook: {hook}, token_id: {tokenId}, local_decimals: {ld}u8, remote_decimals: {rd}u8 }"
+1 "{ default_hook: {aleoMailboxDefaultHook}, required_hook: {aleoMailboxRequiredHook} }"
+2 "{ domain: {dest}u32, recipient: {aleoRemoteRouterRecipient}, gas: {aleoRemoteRouterGas}u128 }"
+3 "{dest}u32"
+4 "[{limb0}u128, {limb1}u128]" (evm/solana address → limbs)
+5 "{amountAtomic}u128"
+6 "[{ spender: S0, amount: {gasPayment}u64 }, { spender: S1, amount: 0u64 }, { spender: S2, amount: 0u64 }, { spender: S3, amount: 0u64 }]"
+```
+Refuses placeholder config, non-active route, missing gasPayment. Receipt SOURCE_CONFIRMING protocolState {routeId, sourceProgram, sourceFunction}.
+IGP quote: read hyp_hook_manager.aleo mapping destination_gas_configs key `{ igp: {aleoMailboxDefaultHook}, destination: {domain}u32 }` → struct {gas_overhead, exchange_rate, gas_price}. gasLimit = aleoRemoteRouterGas (0 → 50000). payment = ((gasLimit + gasOverhead) * gasPrice * exchangeRate) // 10_000_000_000 ; 0 < payment <= 2^64-1. Vector: gas_overhead 159337, exchange_rate 402, gas_price 1000000000, gasLimit 44000 → 8174147.
+
+### 3.4 Aleo xReserve burn
+ethereumDestinationDomain must be 0. amount > withdrawalFeeAtomic strictly. args amount "{atomic}u128", nativeDomain "0u32", nativeRecipient = xReserveHexToAleoBytes(evmAddressToXReserveBytes32(recipient), 32).
+| mode | program | fn | inputs |
+| private (default) | wrapperProgram shielded_usdcx_wrapper.aleo | private_burn | [userRecord, amount, nativeDomain, nativeRecipient, merkleProof] |
+| public | bridgeProgram usdcx_bridge_v2.aleo | burn_public | [amount, nativeDomain, nativeRecipient] |
+| public-as-signer | bridgeProgram | burn_public_as_signer | same |
+Private: userRecord required (Token record of remoteToken program); merkleProof `[MerkleProof; 2]` literal required (no default). Receipt SOURCE_CONFIRMING; protocolState {routeId, burnMode, amountAtomic, nativeDomain, nativeRecipientBytes32, sourceProgram, sourceFunction, forwardingService:'aleo-burn-attestation'}.
+
+### 3.5 Aleo xReserve private mint
+Preconditions: attestation complete; payload/messageHash match; keccak256(attestation.payload) == messageHash. Secret nonce check: expectedHookData = buildXReserveHookData('private', recipient, env, secretNonce); attestedHookData = '0x' + payload.slice(-130) (last 65 bytes); must match. secretNonce default '0scalar'.
+Call wrapperProgram.private_mint with 5 inputs: [xReserveHexToAleoBytes(payload,305), xReserveHexToAleoBytes(attestation,65), xReserveHexToAleoBytes(messageHash,32), secretNonce, recipient]. Receipt DESTINATION_CONFIRMING.
+
+### 3.6 Solana Hyperlane SOL transfer_remote
+Instruction data 77 bytes LE: [0..8) 01x8 discriminator; [8] 0x01 variant; [9..13) destination u32 LE; [13..45) recipient 32 bytes (aleoAddressToBytes32, no reversal); [45..77) amount u256 LE.
+PDAs: dispatched-message = PDA(mailboxProgram, ['hyperlane','-','dispatched_message','-', uniqueMessagePubkey]); gas-payment = PDA(igpProgram, ['hyperlane_igp','-','gas_payment','-', uniqueMessagePubkey]).
+Accounts (16; 15 without overhead):
+```
+0 System 11111111111111111111111111111111 ro ; 1 splNoop ro ; 2 tokenPda ro ; 3 mailboxProgram ro ; 4 mailboxOutboxPda rw
+5 dispatchAuthorityPda ro ; 6 sender signer rw ; 7 uniqueMessage signer ro ; 8 dispatchedMessagePda rw
+9 igpProgram ro ; 10 igpProgramDataPda rw ; 11 gasPaymentPda rw ; 12 igpOverheadAccount ro (OPTIONAL, omitted if absent)
+13 igpAccount rw ; 14 System ro ; 15 nativeCollateralPda rw
+```
+IGP account decode: [1B initialized][8B "IGP_____"][1B bump][32B salt][1B owner Option tag (+32B)][32B beneficiary][4B count u32 LE]; entries 38B: [4B domain u32][1B oracle tag must be 0][16B token_exchange_rate u128][16B gas_price u128][1B token_decimals]. dest_cost = gasAmount*gas_price; origin_cost = dest_cost*exchange_rate // 10^19; lamports = origin_cost * 10^(9−dec) if dec<=9 else // 10^(dec−9). gasAmount = destinationGasAmount '464000'. Vector: exchange_rate 751705303136, gas_price 83169, dec 6 → 2_900_000 lamports.
+Quote: IGP account data, throwaway unique signer, blockhash (confirmed), v0 message with compute unit limit 400_000; getFeeForMessage; rent for 141, 194, 0 bytes; total = amount + igp + fee + rent. plan.sender required.
+Execute: wallet addr == plan.sender; re-quote; preflight balance; partial sign with unique-message keypair; wallet signs fee payer; send. Checkpoint SOURCE_CONFIRMING with {routeId, signature, uniqueMessageAddress, destinationDomain, quotedLamports, blockhash, lastValidBlockHeight} BEFORE polling. Poll getSignatureStatuses(searchTransactionHistory); failed → throw; confirmed|finalized → done; else isBlockhashValid → 'expired'; read exceptions swallowed. Message id: getTransaction logs (maxSupportedTransactionVersion 0, commitment confirmed), regex `Dispatched message to \d+, ID (0x[0-9a-fA-F]{64})`; absence → messageIdUnavailable true.
+
+### 3.7 Delivery verification
+- Hyperlane → Aleo: mapping `deliveries` on hyp_mailbox.aleo key `{ id: [{lo}u128, {hi}u128] }` with lo = LE u128 of bytes[0..16), hi = bytes[16..32). Non-null → delivered. Vector: 0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d → `{ id: [262854447642257427123071959211115528903u128, 102980212169860384794748804418278302317u128] }`.
+- Aleo → EVM: Mailbox `delivered(bytes32) view returns (bool)`.
+- Aleo → Solana: balance-diff fallback only.
+- xReserve → Aleo: mapping `nullifier` on bridgeProgram key xReserveHexToAleoBytes(nonce, 32); value 'true' → delivered.
+- Hyperlane explorer never used in production status.
+
+### 3.8 Encoders
+aleoAddressToBytes32: 'aleo1' prefix, len 63, bech32m (const 0x2bc830a3, alphabet qpzry9x8gf2tvdw0s3jn54khce6mua7l, generators 3b6a57b2 26508e6d 1ea119fa 3d4233dd 2a1462b3), 32 bytes. Vector aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n → 0xb102e0d37e02ec5dbba2460287ac07ef7ea8ee636392ce235402308299901811.
+bytes32ToAleoAddress: inverse.
+evmAddressToXReserveBytes32: left-pad checksummed address to 32.
+evmAddressToAleoHyperlaneRecipient: pad to 32, limbs LE u128 of [0..16),[16..32). Vector 0x1e196d0a7d8189054c4db744ab3340c3f1c68b19 → [13858749752514421660238621190289096704, 33956464229475118999063216025592496509]; 0x…0001 → [0, 1329227995784915872903807060280344576].
+solanaAddressToAleoHyperlaneRecipient: bs58 decode 32 bytes, same limbs. 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7 → [127878782877948140186055645953777992816, 163261512394675613100746600600636171918].
+calculateXReserveDepositNonce = keccak256(abi.encode(uint32 sourceDomain) || txHash(32) || abi.encode(uint256 logIndex)).
+buildXReserveDepositPayload (305 bytes): [0..8) 5a2e0acd00000001; [8..40) amount u256 BE; [40..44) remoteDomain u32 BE; [44..76) remoteToken; [76..108) remoteRecipient; [108..140) localToken padded; [140..172) depositor padded; [172..204) maxFee u256 BE; [204..236) nonce; [236..240) 0x00000041; [240..305) hookData 65.
+buildXReserveHookData(mode, recipient, env, secretNonce='0scalar'): 65 bytes; byte0 = 0/1/2; private: bytes[1..33) = BHP256.commit(Plaintext(recipient).toBitsLe(), Scalar(secretNonce)).toBytesLe().
+xReserveHexToAleoBytes(hex, n): `[0u8,255u8]` NO spaces.
+parseDecimalAmount / formatDecimalAmount: strict regex `^(\d+)(?:\.(\d+))?$`, frac digits <= decimals; format strips trailing zeros.
+
+## 4. TEST VECTORS — see test/utils/*.test.ts, test/solana/*.test.ts, test/actions/*.test.ts, fixtures sealevel-transfer-remote.json (mainnet tx cWFKiumuvVuvrxM8xtunZxNM4FNUppSdyNm7HEqKjV3ZmENebD4DAf44kbyvq9fKJ61VzNrH3tYpLJgUrY8MEGW, instruction base64 AQEBAQEBAQEBb2VsYRw0lpkefGEc7V7lzQze6WnFPvyKVJeuBQgZse8A7SkSACqpcJ0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, amount 676200000000, recipient aleo1rs6fdxg703s3em27uhxsehhfd8znaly22jt6upggrxc77q8d9yfq33pk28), sealevel-igp-account.json (JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M). Solana rent vectors 141→1_872_240, 194→2_241_120, 0→890_880; fee 10_000.
+Live gates: BRIDGE_LIVE_FUNDS=1 + BRIDGE_LIVE_STATE_DIR; BRIDGE_LIVE_MAINNET_ACK=I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS + BRIDGE_LIVE_MAINNET_CASES; BRIDGE_LIVE_MAINNET_EXECUTE=I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS. Hyperlane minimum = one atomic unit; xReserve = 2 USDC.
+
+## 5. AGENT/MCP — three read-only tools: bridge_list_assets, bridge_list_routes, bridge_quote_transfer. No execute tool.
+
+## 6. EXTERNAL — Circle attestation mainnet/testnet URLs above; DEFAULT_SOLANA_RPC_URL https://api.mainnet-beta.solana.com; Hyperlane explorer GraphQL https://explorer4.hasura.app/v1/graphql (live tests only).
+
+## 7. INVARIANTS
+1. Re-resolve route from live registry by id; refuse if plan.registryVersion != registry.version.
+2. Checkpoint is an allowlist.
+3. Idempotent Aleo rebroadcast (prove → persist bytes → broadcast; duplicate = success).
+4. Exact integer arithmetic (hook payment, IGP) — on-chain equality asserts.
+5. Timeout != failure.
+6. getStatus checks destination nullifier first for inbound xReserve.
+7. Private xReserve deposit recipient = wrapper program's address.
+8. xReserveHexToAleoBytes has no spaces; struct/array literals elsewhere do.
diff --git a/bridge-sdk/pyproject.toml b/bridge-sdk/pyproject.toml
new file mode 100644
index 0000000..16ebf33
--- /dev/null
+++ b/bridge-sdk/pyproject.toml
@@ -0,0 +1,26 @@
+[project]
+name = "aleo-bridge-sdk"
+version = "0.1.0"
+description = "Python SDK for bridging assets between Aleo, Ethereum and Solana over Hyperlane warp routes and Circle xReserve"
+readme = "README.md"
+requires-python = ">=3.10"
+# PyNaCl is NOT optional: every Aleo-side execution goes through delegated proving
+# (`proving="delegate"`, the default), and the proving request is sealed with a NaCl box before it
+# leaves the machine — without it `complete`/`execute` raise ImportError at the last step, after the
+# source transaction is already on chain. `aleo-sdk[dps]` pulls the same package.
+dependencies = ["aleo-sdk>=0.5.0", "requests>=2", "pynacl>=1.5"]
+
+[project.optional-dependencies]
+evm = ["web3>=7,<9", "eth-account>=0.13"]
+solana = ["solders>=0.21", "solana>=0.35"]
+# mcp 2.0 renamed the Server registration API — pin to 1.x (same rule as shield-swap).
+mcp = ["mcp>=1.0,<2"]
+dev = ["pytest>=8", "pytest-asyncio>=0.23", "web3>=7,<9", "eth-account>=0.13",
+ "solders>=0.21", "solana>=0.35", "mcp>=1.0,<2", "pynacl>=1.5"]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["python/aleo_bridge"]
diff --git a/bridge-sdk/pyrightconfig.json b/bridge-sdk/pyrightconfig.json
new file mode 100644
index 0000000..973333f
--- /dev/null
+++ b/bridge-sdk/pyrightconfig.json
@@ -0,0 +1,7 @@
+{
+ "include": ["python/aleo_bridge"],
+ "extraPaths": ["python"],
+ "venvPath": ".",
+ "venv": ".venv",
+ "reportMissingModuleSource": false
+}
diff --git a/bridge-sdk/pytest.ini b/bridge-sdk/pytest.ini
new file mode 100644
index 0000000..3204b8a
--- /dev/null
+++ b/bridge-sdk/pytest.ini
@@ -0,0 +1,7 @@
+[pytest]
+pythonpath = python
+testpaths = tests
+markers =
+ live: read-only tests against the REAL mainnet API (BRIDGE_LIVE_READS=1 and -m live)
+addopts = -m "not live"
+asyncio_mode = auto
diff --git a/bridge-sdk/python/aleo_bridge/AGENTS.md b/bridge-sdk/python/aleo_bridge/AGENTS.md
new file mode 100644
index 0000000..93291cb
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/AGENTS.md
@@ -0,0 +1,331 @@
+# aleo-bridge — agent guide
+
+> GENERATED from SDK docstrings by `codegen/gen_context.py` — do not
+> edit by hand; edit the docstrings and regenerate.
+
+Typed Python client that moves assets between Aleo, Ethereum and Solana
+over the reviewed Hyperlane warp routes and Circle xReserve deployments
+(`pip install aleo-bridge-sdk[evm,solana]`, imports as `aleo_bridge`).
+MCP alternative: `python -m aleo_bridge.mcp` exposes the same lifecycle as
+tools; `aleo_bridge.agent.bridge_tools()` gives Claude-shape tool schemas.
+Registry version `2026-08-31.solana-deposits.1`.
+
+## Tier 1 — the lifecycle (quote → execute → wait, then resume / complete as asked)
+
+```python
+from aleo_bridge import Bridge
+
+bridge = Bridge.from_env() # BRIDGE_PRIVATE_KEY (+ EVM/Solana keys) from the environment
+print(bridge.status()) # addresses, balances, pending transfers
+quote = bridge.quote("ethereum/wbtc", "aleo/wbtc", amount="0.001", recipient=bridge.aleo_address())
+print(quote.fees, quote.amount_out) # show these to the user BEFORE executing
+progress = bridge.execute(quote.plan) # source step; checkpoints saved to the bound store
+progress = bridge.wait(progress) # stops at resume / complete / done / failed
+if progress.next == "resume": progress = bridge.wait(bridge.resume(progress))
+if progress.next == "complete": progress = bridge.wait(bridge.complete(progress, secret_nonce=nonce))
+assert progress.next == "done", progress.error
+```
+
+### `from_env(**overrides: 'Any') -> "'Bridge'"`
+
+Everything from the environment (spec §3.3); writes nothing to disk. Overrides: ethereum, solana, registry, checkpoints.
+
+### `from_profile(home: 'Any' = None, *, network: 'str | None' = None, endpoint: 'str | None' = None, ethereum: 'Any' = None, solana: 'Any' = None) -> "'Bridge'"`
+
+The client for the local profile (spec §3.4), created on first use. *network*/*endpoint* apply only when
+creating. Side-chain connections come from the arguments or the same env variables as ``from_env``.
+
+### `status(self) -> 'BridgeStatus'`
+
+Read-only re-orientation: addresses and public balances of every registry asset per configured chain.
+Plan 4 fills ``pending`` from the checkpoint store.
+
+### `quote(self, source, destination, *, amount=None, amount_atomic=None, recipient: 'str', sender: 'str | None' = None, protocol: 'str | None' = None, mint_mode: 'str' = 'public', secret_nonce: 'str' = '0scalar')`
+
+Price a transfer and get the plan that ``execute`` takes. Nothing is signed.
+
+``source`` / ``destination`` are ``"chain/key"`` strings or ``(chain, key)``
+tuples (``"ethereum/usdc"``, ``"aleo/usdcx"``); give exactly one of
+``amount`` (human units, str) or ``amount_atomic`` (int). ``recipient`` is
+the destination-chain address. ``mint_mode`` (xReserve into Aleo only):
+``"public"`` balance, ``"record"`` minted by the relayer, or ``"private"``
+— you finish it yourself with ``complete`` and must keep ``secret_nonce``.
+Returns a kind-specific ``Quote`` (``quote.kind`` in evm-hyperlane /
+solana-hyperlane / aleo-hyperlane / evm-xreserve / aleo-xreserve) with
+``fees`` and ``amount_out`` in human units and ``quote.plan``. Show the
+user fees + amount before ``execute``.
+
+### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, gas_payment_microcredits: 'int | None' = None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0)`
+
+Commit funds on the source chain for ``quote.plan``; returns ``Progress``.
+
+Runs approval(s) → deposit / dispatch / burn, emitting a ``Checkpoint`` to
+``on_checkpoint`` (and the bound store) at every boundary — including
+AFTER proving and BEFORE broadcast for Aleo legs, so a crash there is
+resumable without proving twice. ``proving`` is ``"delegate"`` (DPS) or
+``"local"``; ``mode`` is ``"caller"|"signer"`` (Aleo Hyperlane) or
+``"private"|"public"|"public-as-signer"`` (Aleo xReserve burn, default
+private; ``record``/``merkle_proof`` optional — the SDK selects a record
+and computes the exclusion proof). The Hyperlane hook payment is
+re-quoted right before proving unless ``gas_payment_microcredits`` is
+pinned. Irreversible once the source step is broadcast: afterwards use
+``wait`` / ``recover``, never ``execute`` again.
+
+### `wait(self, progress, *, until=None, poll_seconds: 'float' = 15.0, timeout_seconds: 'float' = 1200.0, on_update=None, on_error=None, max_consecutive_errors: 'int' = 5)`
+
+Poll until the transfer finishes or needs you: stops at ``progress.next``
+in resume / complete / done / failed, or at any status in ``until``.
+
+A ``PollingTimeoutError`` is NOT a failure — the transfer is still in
+flight; call ``wait`` again or ``recover`` later. ``on_update`` receives
+each changed ``Progress``. A transient error (flaky RPC/HTTP transport)
+is retried up to ``max_consecutive_errors`` times, calling ``on_error``
+on each tolerated retry; a non-transient error propagates immediately.
+
+### `recover(self, checkpoint)`
+
+Rebuild ``Progress`` from a saved checkpoint (``Checkpoint``, dict or JSON) — reads only.
+
+Re-resolves the route from the live registry and reads chain state once;
+``progress.next`` then says what to do: ``wait``, ``resume``, ``complete``,
+``done`` or ``failed``.
+
+### `resume(self, progress, *, on_checkpoint=None, secret_nonce: 'str | None' = None, poll_seconds: 'float' = 1.0, timeout_seconds: 'float' = 120.0, proving: 'str' = 'delegate')`
+
+Finish an interrupted source submission (``progress.next == "resume"``).
+
+Rebroadcasts the identical proved Aleo transaction (a duplicate response is
+success) or, on EVM, re-scans history and only then authorizes the single
+missing deposit/dispatch. Never repeats a confirmed step.
+
+### `complete(self, progress, *, secret_nonce: 'str', on_checkpoint=None, proving: 'str' = 'delegate')`
+
+Submit the private USDCx mint (``progress.next == "complete"``).
+
+Requires the same ``secret_nonce`` given to ``execute``; the SDK never
+stored it. Submits exactly one ``private_mint`` and returns
+``DESTINATION_CONFIRMING`` progress to ``wait`` on.
+
+### `pending(self) -> 'list'`
+
+The in-flight transfers of this profile — every checkpoint in the bound store,
+reconstructed offline (:func:`lifecycle.progress_from_checkpoint`): no network read, so one
+unreachable chain can never hide the others. A malformed checkpoint yields a ``Progress``
+with ``next == "failed"`` and ``error`` set instead of raising; call ``wait()``/``recover()``
+on any entry to refresh it against live chain state.
+
+## Serving a chatting user (the conversation pattern)
+
+### Keys and identity
+
+1. **NEVER ask the user to paste a private key into the conversation.** Keys
+ come from the environment only: `BRIDGE_PRIVATE_KEY` (Aleo),
+ `EVM_PRIVATE_KEY` + `ETHEREUM_RPC_URL`, `SOLANA_PRIVATE_KEY` (+ optional
+ `SOLANA_RPC_URL`), set in the user's own shell before the process starts.
+ `Bridge.from_profile()` creates an Aleo key on first use and never writes
+ EVM/Solana keys to disk.
+2. `status()` first in any session: which chains are configured, balances of
+ every bridge asset, and the pending transfers in the checkpoint store. A
+ pending transfer is finished with `recover` → `wait`/`resume`/`complete`,
+ never by starting a new one.
+
+### Quote first, always
+
+3. **Always `quote` before `execute`** and show the user the route, the fees
+ and `amount_out` in human units with symbols ("2 USDC → 2 USDCx; Hyperlane
+ hook payment 8.17 ALEO"), never raw atomic units. Minimums: xReserve
+ needs at least 2 USDC in and strictly more than the 2 USDCx withdrawal fee
+ out; Hyperlane moves one atomic unit but network fees and the relayer
+ payment cost more than that — say so.
+4. Only `execute` after the user confirms. Through the agent tools every
+ write requires `confirm=true`; without it the tool returns the quote and
+ moves nothing. A live mainnet execution additionally needs the user's
+ own `BRIDGE_LIVE_MAINNET_EXECUTE` acknowledgement — never set it yourself;
+ without it, treat any mainnet run as a rehearsal.
+
+### The source step is irreversible
+
+5. Once the deposit / dispatch / burn is broadcast the funds are committed.
+ A timeout, an RPC error or a crash after that point is an UNKNOWN outcome,
+ not a failure: recover from the last checkpoint (`recover(checkpoint)` or
+ `pending()`) — never run `execute` again for the same transfer. This is
+ the funds-safety rule above all others: never resend after an ambiguous
+ broadcast.
+
+### What `progress.next` means for the user
+
+| `progress.next` | Status | Tell the user | Do |
+| --- | --- | --- | --- |
+| `wait` | source confirming, attestation pending, delivery pending | "In flight; I'll keep checking." | `wait(progress)` (or re-check later from the checkpoint) |
+| `resume` | `SOURCE_SUBMISSION_PENDING` | "An approval confirmed / a proof was built but the transfer itself was not submitted; I can submit it now." | confirm, then `resume(progress)` |
+| `complete` | `DESTINATION_ACTION_REQUIRED` | "Circle attested your deposit; your private mint needs your signature (and the secret nonce)." | confirm, then `complete(progress, secret_nonce=...)` |
+| `done` | `COMPLETED` | "Delivered." Report source and destination transaction ids. | nothing |
+| `failed` | `FAILED` / `EXPIRED` | Relay `progress.error`; the source step did not commit funds or was rejected. | nothing — a new transfer needs a new quote |
+
+`wait` raising `PollingTimeoutError` is NOT a failure — say the transfer is
+still in flight and check again later.
+
+### Private mints and the secret nonce
+
+6. `mint_mode="private"` (USDC → USDCx) commits `(recipient, secret_nonce)` on
+ Ethereum. The same `secret_nonce` is required by `complete`; the SDK
+ **never stores** it and checkpoints exclude it (and every other secret).
+ Tell the user to keep it (the default `0scalar` needs no storage but adds
+ no entropy). Only the recipient's Aleo key can complete a private mint —
+ make sure the recipient IS the configured Aleo address before depositing.
+7. Aleo-origin Hyperlane transfers spend PUBLIC balances: `unshield` a private
+ record first. Hyperlane delivers into public balances; `shield` afterwards
+ if the user wants privacy. Private xReserve burns spend records directly.
+
+### While acting
+
+8. Writes are slow (proving + confirmation ≈ a minute or two on Aleo; Circle
+ attestation and Hyperlane relay take minutes). Never re-submit because a
+ call seems slow — `status()` / `recover` first.
+9. Confirm, act, report ids. Errors name their own fix — read the exception
+ message and do what it says.
+
+
+## Tier 2 — the protocol modules (building your own flows)
+
+Every Aleo write returns an `AleoCall`: nothing touches the network until
+`.simulate()` (free), `.prove()` / `.delegate_prepared()` (proved, not
+broadcast — checkpoint it), `.submit_prepared()`, `.transact()` (local
+proving + broadcast) or `.delegate()` (DPS + broadcast). EVM and Solana
+writes return `EvmCall` / `SolCall` with `.build()` (unsigned) and `.send()`.
+The lifecycle verbs above compose these; use them directly only when you
+need a single leg. Confirm-gated writes and the never-resend rule above
+apply here too — these are the same broadcasts, just one leg at a time.
+
+### `hyperlane.transfer_remote(self, asset: 'Any', recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, as_signer: 'bool' = False, gas_payment_microcredits: 'int | None' = None) -> 'AleoCall[DispatchReceipt]'`
+
+Withdraw an Aleo warp asset to Ethereum/Solana. Quotes the IGP payment now unless pinned; the
+lifecycle layer (plan 4) re-quotes at the last responsible moment by calling this again.
+
+### `hyperlane.quote_gas_payment(self, asset: 'Any') -> 'GasQuote'`
+
+Live relayer payment for the route (the exact u64 the hook asserts); quote right before proving.
+
+### `xreserve.burn(self, recipient: 'str', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mode: 'str' = 'private', record: 'str | None' = None, merkle_proof: 'str | None' = None) -> 'AleoCall[BurnReceipt]'`
+
+Burn USDCx for USDC on Ethereum. ``private`` (default) spends a Token record via the wrapper and needs a
+freeze-list exclusion proof — both are resolved from chain state when not supplied. Minimum: more than
+the 2 USDCx withdrawal fee. The Aleo burn-attestation service forwards accepted burns to Circle.
+
+### `xreserve.private_mint(self, attestation: 'Attestation', recipient: 'str', *, secret_nonce: 'str' = '0scalar', route: 'Route | None' = None) -> 'AleoCall[MintReceipt]'`
+
+Finish a private-mode deposit: the only user-signed Aleo step of the inbound flow (``wrapper.private_mint``).
+
+### `xreserve.get_attestation(self, message_hash: "'str | bytes'", *, route: 'Route | None' = None) -> 'Attestation | None'`
+
+One Circle request for *message_hash*; ``None`` while pending (404).
+
+### `shield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`
+
+
+
+### `unshield(self, asset: 'Any', *, amount: 'Any' = None, amount_atomic: 'int | None' = None, record: 'str | None' = None, merkle_proof: 'str | None' = None, recipient: 'str | None' = None) -> 'AleoCall[PrivacyReceipt]'`
+
+
+
+### `freezelist.exclusion_proof(self, address: 'str', program: 'str') -> 'str'`
+
+``[MerkleProof; 2]`` proving *address* is not frozen on *program*; veil's empty pair when the list is empty.
+
+### `eth.transfer_remote(self, asset: 'Any' = None, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, plan: 'Plan | None' = None) -> 'EvmCall[DispatchReceipt]'`
+
+Send ETH, WBTC or USDT to Aleo through its Hyperlane Warp Route.
+
+Re-quotes ``quoteTransferRemote`` at send time. Collateral routes approve exactly the
+quoted token amount only when the allowance is short (USDT: a non-zero allowance is
+reset to 0 first). Native ETH sends amount + fee as ``msg.value``; collateral routes
+send the fee only. Each hash is checkpointed before polling; a timeout returns a
+pending ``DispatchReceipt``. The message id comes from the Mailbox ``DispatchId`` log.
+
+``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+re-resolved by id against the live registry, the sender must be the connected account, and
+the plan must equal what this call would have prepared itself. Mutually exclusive with ``asset=``.
+
+### `eth.deposit_usdc(self, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, mint_mode: 'str | None' = None, secret_nonce: 'str' = '0scalar', plan: 'Plan | None' = None) -> 'EvmCall[DepositReceipt]'`
+
+Deposit USDC into Circle xReserve for USDCx on Aleo (minimum 2 USDC; irreversible once confirmed).
+
+``mint_mode``: ``public`` (public USDCx balance), ``record`` (protocol-minted private
+record), or ``private`` (deposit addressed to the shielded wrapper program; you must later
+run ``bridge.xreserve.private_mint`` / plan 4's ``complete`` with the same ``secret_nonce``,
+which the SDK never stores). Approves exactly the amount only when the allowance is
+short, then ``depositToRemote`` with no ``msg.value``. The confirmed ``DepositReceipt``
+carries Circle's message hash (receipt id) and the deposit nonce. ``mint_mode`` defaults to
+``plan.mint_mode`` when a plan is given, else ``"public"``.
+
+``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+re-resolved by id against the live registry, the sender must be the connected account, and
+the plan must equal what this call would have prepared itself. ``secret_nonce`` is never
+part of a plan, so a private deposit must still pass the same one it was quoted with.
+
+### `eth.quote_transfer_remote(self, asset: 'Any' = None, recipient: 'str | None' = None, *, amount: 'Any' = None, amount_atomic: 'int | None' = None, route: 'Route | None' = None, sender: 'str | None' = None, plan: 'Plan | None' = None) -> 'EvmHyperlaneQuote'`
+
+Quote an Ethereum → Aleo Hyperlane transfer without signing.
+
+Native routes (ETH): ``msg.value`` carries the asset and the relayer fee, so
+``native_fee_atomic = native_value_atomic - amount``. Collateral routes (WBTC, USDT):
+``msg.value`` is fee only and ``approval_required`` reflects the router's ERC-20
+allowance for ``sender`` (or the connection's account); it is ``None`` when no account is known.
+
+``plan=`` re-quotes a plan prepared earlier: it supplies the route, sender, recipient and
+amount, and is validated against the live registry. It is mutually exclusive with
+``asset=``/``route=``/``sender=``.
+
+### `sol.transfer_remote(self, recipient: 'str | None' = None, *, amount: 'str | None' = None, amount_atomic: 'int | None' = None, plan: 'Plan | None' = None) -> 'SolCall[DispatchReceipt]'`
+
+Send native SOL to an Aleo address over the Hyperlane warp route (spec §6).
+
+Returns a :class:`SolCall`: ``build()`` previews the partially signed transaction,
+``send()`` moves funds (amount + IGP payment + network fee + rent leave the wallet).
+
+``plan`` (from ``Bridge.execute``) supplies recipient and amount and must have been prepared for
+the connected wallet; its registry version and route id are re-checked against the live registry
+when the call runs. An ``amount``/``amount_atomic`` that disagrees with the plan is a
+``ValueError``. Without a plan, ``recipient`` is required.
+
+### `sol.quote_transfer_remote(self, recipient: 'str | None' = None, *, amount: 'str | None' = None, amount_atomic: 'int | None' = None, sender: 'str | None' = None, plan: 'Plan | None' = None) -> 'SolanaHyperlaneQuote'`
+
+Lamports required for a SOL → Aleo transfer: amount + IGP payment + network fee + rent (spec §5 kind
+``solana-hyperlane``). Reads Solana; never signs. ``sender`` defaults to the connected wallet and is required
+for the fee estimate.
+
+``plan`` (from ``Bridge.quote``) supplies recipient, amount and sender, and must match the live registry
+version and route; like ``EthModule`` it is mutually exclusive with ``sender=``, and an ``amount``/
+``amount_atomic`` that disagrees with the plan is a ``ValueError`` (an identical one is tolerated, so
+re-stating the plan's own amount is harmless). Without a plan, ``recipient`` is required.
+
+### Routes in the pinned registry
+
+| Route id | Protocol | Environment | Availability |
+| --- | --- | --- | --- |
+| `xreserve:ethereum/usdc->aleo/usdcx` | xreserve | mainnet | active |
+| `xreserve:aleo/usdcx->ethereum/usdc` | xreserve | mainnet | active |
+| `xreserve:sepolia/usdc->aleo-testnet/usdcx` | xreserve | testnet | active |
+| `xreserve:aleo-testnet/usdcx->sepolia/usdc` | xreserve | testnet | active |
+| `hyperlane:ethereum/eth->aleo/eth` | hyperlane | mainnet | active |
+| `hyperlane:aleo/eth->ethereum/eth` | hyperlane | mainnet | active |
+| `hyperlane:ethereum/wbtc->aleo/wbtc` | hyperlane | mainnet | active |
+| `hyperlane:aleo/wbtc->ethereum/wbtc` | hyperlane | mainnet | active |
+| `hyperlane:ethereum/usdt->aleo/usdt` | hyperlane | mainnet | active |
+| `hyperlane:aleo/usdt->ethereum/usdt` | hyperlane | mainnet | active |
+| `hyperlane:solana/sol->aleo/sol` | hyperlane | mainnet | active |
+| `hyperlane:aleo/sol->solana/sol` | hyperlane | mainnet | active |
+| `hyperlane:aleo/aleo->ethereum/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:ethereum/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->solana/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:solana/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->base/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:base/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/aleo->hyperevm/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:hyperevm/aleo->aleo/aleo` | hyperlane | mainnet | metadata-required |
+| `hyperlane:ethereum/usad->aleo/usad` | hyperlane | mainnet | metadata-required |
+| `hyperlane:aleo/usad->ethereum/usad` | hyperlane | mainnet | metadata-required |
+
+`metadata-required` routes are listed but refused by `quote`/`execute`
+until their deployments are reviewed upstream.
+
diff --git a/bridge-sdk/python/aleo_bridge/__init__.py b/bridge-sdk/python/aleo_bridge/__init__.py
new file mode 100644
index 0000000..7d2aabb
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/__init__.py
@@ -0,0 +1,78 @@
+"""aleo_bridge — move assets between Aleo, Ethereum and Solana (Hyperlane warp routes, Circle xReserve).
+
+Web3.py idioms: bind an ``aleo.Aleo`` facade to :class:`Bridge`; reads return values, Aleo writes
+return an :class:`AleoCall` with ``simulate()`` / ``prove()`` / ``transact()`` / ``delegate()``.
+Exports grow in Tasks 4–11 of plan 1; keep this list sorted.
+"""
+from __future__ import annotations
+
+from pathlib import Path
+
+__version__ = "0.1.0"
+
+from .errors import ( # noqa: E402
+ AmbiguousRouteError, AttestationError, BridgeError, ChainMismatchError, CheckpointInvalidError,
+ ConfigurationError, DeliveryUnknownError, InsufficientBalanceError, InvalidAmountError,
+ InvalidRecipientError, MissingExtraError, NotResumableError, PollingTimeoutError,
+ RegistryVersionMismatchError, RouteNotFoundError, RouteUnavailableError, UnsupportedRouteError,
+)
+from .registry import DEFAULT_REGISTRY, Asset, Chain, Locator, Privacy, Registry, Route, validate_registry # noqa: E402
+from .types import ( # noqa: E402
+ CALLER_BOUNDARIES, TERMINAL, AleoHyperlaneQuote, AleoXReserveQuote, Attestation, BridgeStatus, BurnReceipt,
+ ChainStatus, DepositReceipt, DispatchReceipt, EvmHyperlaneQuote, EvmXReserveQuote, Fee, GasQuote,
+ MintReceipt, Plan, PreparedTx, PrivacyReceipt, Progress, Quote, Receipt, SolanaHyperlaneQuote, Status, Step,
+ to_progress,
+)
+from ._calls import AleoCall, EvmCall, SolCall # noqa: E402
+from .checkpoint import Checkpoint, CheckpointStore, FileCheckpointStore, create_checkpoint # noqa: E402
+from .circle import CircleClient # noqa: E402
+from .client import Bridge # noqa: E402
+from .eth import Ethereum, EthModule # noqa: E402
+from .freezelist import EMPTY_MERKLE_PROOF_PAIR, FreezeList # noqa: E402
+from .hyperlane import HyperlaneModule # noqa: E402
+from . import lifecycle # noqa: E402
+from .lifecycle import prepare # noqa: E402
+from .agent import bridge_tools, dispatch_tool # noqa: E402
+from .privacy import PrivacyModule # noqa: E402
+from .profile import DEFAULT_ENDPOINT, Profile # noqa: E402
+from .sol import DEFAULT_SOLANA_RPC_URL, Solana, SolModule # noqa: E402
+from .xreserve import XReserveModule # noqa: E402
+
+__all__ = [
+ "__version__", "AmbiguousRouteError", "AttestationError", "BridgeError", "ChainMismatchError",
+ "CheckpointInvalidError", "ConfigurationError", "DeliveryUnknownError", "InsufficientBalanceError",
+ "InvalidAmountError", "InvalidRecipientError", "MissingExtraError", "NotResumableError",
+ "PollingTimeoutError", "RegistryVersionMismatchError", "RouteNotFoundError", "RouteUnavailableError",
+ "UnsupportedRouteError",
+ "Asset", "Chain", "DEFAULT_REGISTRY", "Locator", "Privacy", "Registry", "Route", "validate_registry",
+ "CALLER_BOUNDARIES", "TERMINAL", "AleoHyperlaneQuote", "AleoXReserveQuote", "Attestation", "BridgeStatus",
+ "BurnReceipt", "ChainStatus", "DepositReceipt", "DispatchReceipt", "EvmHyperlaneQuote", "EvmXReserveQuote",
+ "Fee", "GasQuote", "MintReceipt", "Plan", "PreparedTx", "PrivacyReceipt", "Progress", "Quote", "Receipt",
+ "SolanaHyperlaneQuote", "Status", "Step", "to_progress",
+ "AleoCall", "Bridge", "CircleClient", "DEFAULT_ENDPOINT", "EMPTY_MERKLE_PROOF_PAIR", "FreezeList",
+ "HyperlaneModule", "PrivacyModule", "Profile", "XReserveModule",
+ "Checkpoint", "CheckpointStore", "FileCheckpointStore", "create_checkpoint",
+ "EthModule", "Ethereum", "EvmCall",
+ "DEFAULT_SOLANA_RPC_URL", "Solana", "SolCall", "SolModule",
+ "lifecycle", "prepare",
+ "agent_guide", "bridge_tools", "dispatch_tool",
+]
+
+AGENTS_FILE = "AGENTS.md"
+
+
+def agent_guide() -> str:
+ """The packaged agent guide: the prose a model reads alongside :func:`bridge_tools`.
+
+ ``AGENTS.md`` is generated from the registry and the lifecycle docstrings by
+ ``codegen/gen_context.py`` and shipped inside the wheel. When it is not present (a source
+ checkout before that step has run), this returns a short pointer instead of failing — the tool
+ definitions themselves always carry their own descriptions.
+ """
+ path = Path(__file__).with_name(AGENTS_FILE)
+ try:
+ return path.read_text(encoding="utf-8")
+ except OSError:
+ return (f"aleo_bridge {__version__}: no packaged {AGENTS_FILE} in this build. Generate it with "
+ "codegen/gen_context.py, or call aleo_bridge.bridge_tools() — every tool carries its "
+ "own description and JSON schema.")
diff --git a/bridge-sdk/python/aleo_bridge/__main__.py b/bridge-sdk/python/aleo_bridge/__main__.py
new file mode 100644
index 0000000..9b845b0
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/__main__.py
@@ -0,0 +1,36 @@
+"""``python -m aleo_bridge`` prints the agent guide; ``[status|routes|assets]`` run those checks
+(``status`` needs BRIDGE_PRIVATE_KEY, read-only)."""
+from __future__ import annotations
+
+import dataclasses
+import json
+import sys
+
+from .registry import DEFAULT_REGISTRY
+
+USAGE = "usage: python -m aleo_bridge [status|routes|assets]"
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = list(sys.argv[1:] if argv is None else argv)
+ if not args:
+ from . import agent_guide
+ print(agent_guide(), end="")
+ return 0
+ command = args[0]
+ if command == "routes":
+ print(json.dumps([r.id for r in DEFAULT_REGISTRY.routes(include_unavailable=True)], indent=1))
+ return 0
+ if command == "assets":
+ print(json.dumps([a.id for a in DEFAULT_REGISTRY.assets()], indent=1))
+ return 0
+ if command == "status":
+ from .client import Bridge
+ print(json.dumps(dataclasses.asdict(Bridge.from_env().status()), indent=1, default=str))
+ return 0
+ print(USAGE, file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__": # pragma: no cover
+ raise SystemExit(main())
diff --git a/bridge-sdk/python/aleo_bridge/_base58.py b/bridge-sdk/python/aleo_bridge/_base58.py
new file mode 100644
index 0000000..3d89212
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_base58.py
@@ -0,0 +1,35 @@
+"""Bitcoin/Solana base58 (no checksum). Vendored so Solana recipients encode without the solana extra."""
+from __future__ import annotations
+
+ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
+_INDEX = {c: i for i, c in enumerate(ALPHABET)}
+
+
+def b58decode(s: str) -> bytes:
+ """Decode *s*; leading ``1`` characters become leading zero bytes. ``ValueError`` on a bad character."""
+ if not isinstance(s, str):
+ raise ValueError("base58 input must be a str")
+ n = 0
+ for ch in s:
+ try:
+ n = n * 58 + _INDEX[ch]
+ except KeyError:
+ raise ValueError(f"Invalid base58 character {ch!r}") from None
+ body = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b""
+ pad = len(s) - len(s.lstrip("1"))
+ return bytes(pad) + body
+
+
+def b58encode(data: bytes) -> str:
+ """Encode *data*; leading zero bytes become leading ``1`` characters."""
+ data = bytes(data)
+ pad = len(data) - len(data.lstrip(b"\x00"))
+ n = int.from_bytes(data, "big")
+ out = []
+ while n:
+ n, rem = divmod(n, 58)
+ out.append(ALPHABET[rem])
+ return "1" * pad + "".join(reversed(out))
+
+
+__all__ = ["ALPHABET", "b58decode", "b58encode"]
diff --git a/bridge-sdk/python/aleo_bridge/_calls.py b/bridge-sdk/python/aleo_bridge/_calls.py
new file mode 100644
index 0000000..ade2dc7
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_calls.py
@@ -0,0 +1,418 @@
+"""AleoCall — a facade BoundCall plus a typed result builder (contract §_calls.py).
+
+The caller picks the proving path; every path harvests the ROOT transition's outputs (the last
+transition whose program/function match the call — Aleo orders child transitions first) so the typed
+result is complete without waiting for confirmation. ``delegate(broadcast=False)`` is the
+checkpointable path: prove on the DPS, hand back the serialized transaction, then ``submit_prepared``
+— where a duplicate-transaction response counts as success (invariant 3).
+"""
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass
+from typing import Any, Callable, Generic, TypeVar
+
+from .errors import BridgeError, ConfigurationError
+from .types import PreparedTx
+
+R = TypeVar("R")
+_DUPLICATE_MARKER = "already exists"
+
+
+def _network_module(aleo: Any) -> Any:
+ import aleo as aleo_pkg
+ return getattr(aleo_pkg, aleo.network_name)
+
+
+def extract_tx_id(payload: Any) -> str:
+ """Transaction id from a DPS result payload (dict variants or a bare id string)."""
+ if isinstance(payload, str) and payload.strip():
+ return payload.strip()
+ if isinstance(payload, dict):
+ tx = payload.get("transaction")
+ if isinstance(tx, dict) and tx.get("id"):
+ return str(tx["id"])
+ for key in ("transaction_id", "transactionId", "id", "txid", "tx_id"):
+ if payload.get(key):
+ return str(payload[key])
+ raise ValueError(f"Cannot find a transaction id in DPS payload: {payload!r}")
+
+
+def payload_transitions(payload: Any) -> "list[dict[str, Any]] | None":
+ """Decoded transitions from a DPS payload that carries the whole transaction; None when it is id-only."""
+ if not isinstance(payload, dict):
+ return None
+ return _transaction_transitions(payload.get("transaction"))
+
+
+def _transaction_transitions(tx: Any) -> "list[dict[str, Any]] | None":
+ if not isinstance(tx, dict):
+ return None
+ transitions = (tx.get("execution") or {}).get("transitions")
+ if not isinstance(transitions, list):
+ return None
+ return [{"program": str(t.get("program")), "function": str(t.get("function")), "outputs": t.get("outputs", [])}
+ for t in transitions]
+
+
+def output_values(outputs: Any) -> list[str]:
+ values: list[str] = []
+ for out in outputs:
+ if isinstance(out, dict):
+ value = out.get("value")
+ values.append(value if isinstance(value, str) else str(value))
+ else:
+ values.append(str(out))
+ return values
+
+
+def root_outputs(decoded: list[dict[str, Any]], program: str, function: str) -> list[str]:
+ """Output values of the LAST transition matching *program*/*function* (the root)."""
+ for entry in reversed(decoded):
+ if str(entry.get("program")) == program and str(entry.get("function")) == function:
+ return output_values(entry.get("outputs", []))
+ return []
+
+
+def is_duplicate_submission(exc: BaseException) -> bool:
+ """True only for a node's "already exists" rejection (an idempotent rebroadcast).
+
+ Deliberately narrow: a message like "duplicate serial number" or "duplicate output id" is a
+ REAL double-spend failure and must propagate, not be swallowed as success.
+ """
+ return _DUPLICATE_MARKER in str(exc).lower()
+
+
+class AleoCall(Generic[R]):
+ """A prepared Aleo write. Nothing touches the network until a verb runs."""
+
+ def __init__(self, aleo: Any, bound: Any, build_result: Callable[[str, list[str]], R], *,
+ imports: "dict[str, str] | None" = None) -> None:
+ self._aleo = aleo
+ self._bound = bound
+ self._build = build_result
+ self._imports = dict(imports or {})
+ self._imports_registered = False
+
+ def __repr__(self) -> str:
+ # Never print .inputs here: a record plaintext (private_burn arg 0) or a secret nonce
+ # (private_mint arg 3) can be an input literal, and repr() output tends to end up in logs.
+ return f"AleoCall({self.program_id}/{self.function_name}, inputs={len(self.inputs)} literals)"
+
+ @property
+ def program_id(self) -> str:
+ return str(self._bound.program_id)
+
+ @property
+ def function_name(self) -> str:
+ return str(self._bound.function_name)
+
+ @property
+ def inputs(self) -> list[str]:
+ """The exact Aleo input literals as they will be submitted."""
+ return list(self._bound.args)
+
+ # ── import registration (program sources the process must know before authorizing) ──
+ def _register_imports(self) -> None:
+ if self._imports_registered or not self._imports:
+ return
+ process = self._aleo.process
+ net = _network_module(self._aleo)
+ for program_id, source in self._imports.items():
+ if process.contains_program(net.ProgramID.from_string(program_id)):
+ continue
+ process.add_program(net.Program.from_source(source))
+ self._imports_registered = True
+
+ # ── verbs ──
+ def simulate(self, account: Any = None) -> Any:
+ """Local authorization — no proof, no network send; inspect outputs before spending."""
+ self._register_imports()
+ return self._bound.simulate(account)
+
+ def prove(self, account: Any = None, **fee: Any) -> PreparedTx:
+ """Prove locally, do NOT broadcast; returns the serialized transaction for checkpointing."""
+ self._register_imports()
+ tx = self._bound.build_transaction(account, **fee)
+ return PreparedTx(transaction_id=str(tx.id), serialized=str(tx.raw))
+
+ def transact(self, account: Any = None, **fee: Any) -> R:
+ """Prove locally, harvest root outputs, broadcast, build the typed result."""
+ self._register_imports()
+ tx = self._bound.build_transaction(account, **fee)
+ outputs = root_outputs(tx.decoded(), self.program_id, self.function_name)
+ self._aleo.network.submit_transaction(tx.raw)
+ return self._build(str(tx.id), outputs)
+
+ def delegate(self, account: Any = None, *, wait: bool = True, wait_timeout: float = 180.0,
+ broadcast: bool = True, **fee: Any) -> R:
+ """Delegate proving to the DPS (fee master pays by default).
+
+ ``broadcast=True``: the prover broadcasts; outputs come from the returned transaction, or after
+ waiting and fetching when the payload is id-only. ``broadcast=False``: ``delegate_prepared`` then
+ ``submit_prepared`` so the exact bytes exist locally before the network sees them.
+
+ When the payload is id-only, ``wait`` is effectively forced ``True`` regardless of what was
+ passed: the transaction body must be fetched after confirmation to harvest outputs, so a wait
+ happens either way in that branch.
+
+ If ``wait`` (or the forced wait above) times out, ``aleo.facade.errors.TransactionConfirmationTimeout``
+ propagates AFTER the transaction has already been broadcast by the DPS — the transaction id is
+ recoverable from the exception's ``tx_id`` attribute (or from re-deriving it) for later polling;
+ the transfer itself was not rolled back.
+ """
+ if not broadcast:
+ return self.submit_prepared(self.delegate_prepared(account, **fee), wait=wait, wait_timeout=wait_timeout)
+ self._register_imports()
+ payload = self._bound.delegate(account, broadcast=True, **fee)
+ tx_id = extract_tx_id(payload)
+ decoded = payload_transitions(payload)
+ if decoded is None:
+ self._aleo.network.wait_for_transaction(tx_id, timeout=wait_timeout)
+ tx = self._aleo.network.get_transaction_object(tx_id)
+ decoded = [{"program": str(t.program_id), "function": str(t.function_name), "outputs": list(t.outputs())}
+ for t in tx.transitions()]
+ elif wait:
+ self._aleo.network.wait_for_transaction(tx_id, timeout=wait_timeout)
+ return self._build(tx_id, root_outputs(decoded, self.program_id, self.function_name))
+
+ def delegate_prepared(self, account: Any = None, **fee: Any) -> PreparedTx:
+ """DPS proves with ``broadcast=False``; returns the serialized transaction for checkpointing."""
+ self._register_imports()
+ payload = self._bound.delegate(account, broadcast=False, **fee)
+ tx = payload.get("transaction") if isinstance(payload, dict) else None
+ if not isinstance(tx, dict) or not tx.get("id"):
+ raise ConfigurationError(
+ "The delegated prover did not return the transaction body; cannot checkpoint an unbroadcast "
+ "transaction. Use delegate(broadcast=True) or prove() instead.")
+ return PreparedTx(transaction_id=str(tx["id"]), serialized=json.dumps(tx))
+
+ def submit_prepared(self, prepared: PreparedTx, *, wait: bool = True, wait_timeout: float = 180.0) -> R:
+ """Broadcast a prepared transaction; a duplicate-transaction rejection is success (idempotent rebroadcast).
+
+ If ``wait`` is true and confirmation does not land within ``wait_timeout``,
+ ``aleo.facade.errors.TransactionConfirmationTimeout`` propagates AFTER the transaction has already
+ been broadcast (the ``submit_transaction`` call above already returned/succeeded) — this is not a
+ submission failure. The transaction id is recoverable from ``prepared.transaction_id`` or from the
+ exception's own ``tx_id`` attribute, for later polling or a checkpoint. Keeping this raise (rather
+ than swallowing it) is consistent with the rest of the facade; the lifecycle layer calls
+ ``submit_prepared(wait=False)`` and does its own status polling instead of relying on this wait.
+ """
+ try:
+ self._aleo.network.submit_transaction(prepared.serialized)
+ except Exception as exc: # noqa: BLE001 — the node's error type varies by transport
+ if not is_duplicate_submission(exc):
+ raise
+ if wait:
+ self._aleo.network.wait_for_transaction(prepared.transaction_id, timeout=wait_timeout)
+ try:
+ decoded = _transaction_transitions(json.loads(prepared.serialized)) or []
+ except (TypeError, ValueError):
+ decoded = []
+ return self._build(prepared.transaction_id, root_outputs(decoded, self.program_id, self.function_name))
+
+
+@dataclass(frozen=True)
+class EvmStep:
+ """One unsigned EVM transaction the call will broadcast, in order."""
+
+ kind: str # "approve" | "main"
+ to: str
+ data: str # 0x calldata
+ value: int = 0 # wei (msg.value)
+
+
+@dataclass(frozen=True)
+class EvmOutcome:
+ """What the step runner observed; the module's ``finish`` turns it into the typed result."""
+
+ status: str # "SOURCE_APPROVAL_PENDING" | "SOURCE_CONFIRMING" | "CONFIRMED"
+ sender: str
+ approval_tx_ids: tuple[str, ...]
+ source_tx_id: str | None
+ receipt: Any | None # web3 receipt when status == "CONFIRMED"
+
+
+def _assert_evm_success(receipt: Any, tx_hash: str) -> None:
+ if int(receipt["status"]) == 0:
+ raise BridgeError(f"EVM transaction reverted: {tx_hash}")
+
+
+class EvmCall(Generic[R]):
+ """A prepared Ethereum write: ``build()`` for unsigned transaction dicts, ``send()`` to broadcast.
+
+ ``steps(sender)`` is evaluated at ``build``/``send`` time so allowances and router
+ fees are read at the last responsible moment. ``send`` broadcasts approvals then the
+ main call, emits a ``Checkpoint`` after every broadcast (before polling) and after
+ confirmation, and returns a pending result when a receipt does not arrive within
+ ``timeout_seconds`` — a timeout is not a failure.
+ """
+
+ def __init__(self, conn: Any, *, plan: "Plan", registry: "Registry",
+ steps: Callable[[str], list[EvmStep]], finish: Callable[[EvmOutcome], R],
+ store: "CheckpointStore | None" = None) -> None:
+ self._conn, self.plan, self._registry = conn, plan, registry
+ self._steps, self._finish, self._store = steps, finish, store
+ self._broadcast_id: str | None = None
+
+ def _record_broadcast(self, tx_hash: str) -> None:
+ """Arm the single-use guard with the FIRST hash this call put on the wire."""
+ if self._broadcast_id is None:
+ self._broadcast_id = tx_hash
+
+ def _refuse_a_second_send(self) -> None:
+ if self._broadcast_id is not None:
+ raise BridgeError(
+ f"this call already broadcast {self._broadcast_id}; use bridge.eth.source_status(plan, receipt) "
+ "to follow it — do not resend")
+
+ def _sender(self) -> str:
+ sender = self._conn.require_address()
+ if self.plan.sender:
+ Web3 = self._conn.w3.__class__
+ if Web3.to_checksum_address(self.plan.sender) != sender:
+ raise ConfigurationError(
+ f"Prepared sender {self.plan.sender} does not match connected account {sender}")
+ return sender
+
+ def build(self) -> list[dict]:
+ """Unsigned transaction dicts in submission order (approvals then main). Reads only.
+
+ With an account configured the plan's sender must be that account (same rule ``send()``
+ applies), so a mismatched plan fails here rather than producing calldata nobody can sign.
+ """
+ sender = self._sender() if self._conn.address is not None else self.plan.sender
+ if sender is None:
+ raise ConfigurationError("build() needs a sender: configure a signer or set plan.sender")
+ nonce = int(self._conn.w3.eth.get_transaction_count(sender, "pending"))
+ return [{"from": sender, "to": step.to, "data": step.data, "value": step.value,
+ "chainId": self._conn.chain_id, "nonce": nonce + i}
+ for i, step in enumerate(self._steps(sender))]
+
+ def _checkpoint(self, result: R, on_checkpoint: Callable[["Checkpoint"], None] | None, tx_hash: str) -> None:
+ """Emit the checkpoint for a just-broadcast *tx_hash* to the caller first, then the store.
+
+ The caller's callback runs before the store because the transaction is already on the wire:
+ if persistence fails, the hash must still have reached the one channel that can act on it.
+ A store failure is then fatal and names the hash — losing it silently would strand funds.
+ """
+ from .checkpoint import create_checkpoint
+
+ checkpoint = create_checkpoint(self.plan, result.receipt, self._registry) # type: ignore[attr-defined]
+ if on_checkpoint is not None:
+ on_checkpoint(checkpoint) # the caller's own callback: errors are theirs
+ if self._store is not None:
+ try:
+ self._store.save(checkpoint)
+ except Exception as exc: # noqa: BLE001 — any store backend failure
+ raise BridgeError(
+ f"Transaction {tx_hash} WAS broadcast but its checkpoint {checkpoint.id} could not be saved "
+ f"({exc}); record the transaction hash before retrying — resending would double-spend") from exc
+
+ def send(self, *, wait: bool = True, timeout_seconds: float = 120.0, poll_seconds: float = 1.0,
+ on_checkpoint: Callable[["Checkpoint"], None] | None = None) -> R:
+ """Broadcast every step in order; checkpoint each hash before polling; pending on timeout.
+
+ ``wait=False`` broadcasts only the first step and returns its pending result; call
+ ``bridge.eth.source_status`` (or plan 4's ``resume``) to continue.
+
+ A call is single-use once ANY of its steps has broadcast: a second ``send()`` raises rather
+ than re-approving and re-dispatching the same funds. The approval and main broadcasts of one
+ ``send()`` are of course fine, and a failure before the first broadcast (a validation error, a
+ failed read) leaves the call usable. ``build()`` stays repeatable — it spends nothing.
+ """
+ self._refuse_a_second_send()
+ sender = self._sender()
+ approvals: list[str] = []
+ for step in self._steps(sender):
+ try:
+ tx_hash = self._conn.send_transaction({"from": sender, "to": step.to, "data": step.data, "value": step.value})
+ except BridgeError as exc:
+ # A send whose RPC response was lost may still have reached the node — arm the guard
+ # with the hash it named, so a retry cannot turn an ambiguous send into a double spend.
+ lost = getattr(exc, "broadcast_id", None)
+ if lost is not None:
+ self._record_broadcast(str(lost))
+ raise
+ self._record_broadcast(tx_hash)
+ if step.kind == "approve":
+ approvals.append(tx_hash)
+ pending = self._finish(EvmOutcome("SOURCE_APPROVAL_PENDING", sender, tuple(approvals), None, None))
+ self._checkpoint(pending, on_checkpoint, tx_hash)
+ if not wait:
+ return pending
+ receipt = self._conn.wait_for_receipt(tx_hash, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds)
+ if receipt is None:
+ return pending
+ _assert_evm_success(receipt, tx_hash)
+ continue
+ pending = self._finish(EvmOutcome("SOURCE_CONFIRMING", sender, tuple(approvals), tx_hash, None))
+ self._checkpoint(pending, on_checkpoint, tx_hash)
+ if not wait:
+ return pending
+ receipt = self._conn.wait_for_receipt(tx_hash, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds)
+ if receipt is None:
+ return pending
+ _assert_evm_success(receipt, tx_hash)
+ confirmed = self._finish(EvmOutcome("CONFIRMED", sender, tuple(approvals), tx_hash, receipt))
+ self._checkpoint(confirmed, on_checkpoint, tx_hash)
+ return confirmed
+ raise BridgeError("EvmCall has no main step")
+
+
+class SolCall(Generic[R]):
+ """A prepared Solana-origin call (spec §7).
+
+ ``build()`` re-quotes, compiles the v0 transaction and signs it with the ephemeral
+ unique-message keypair only — a preview that spends nothing. ``send()`` rebuilds with a
+ fresh quote, unique key and blockhash, checks the wallet balance, adds the fee-payer
+ signature through the connection's signer, broadcasts, hands the ``Checkpoint`` built from the
+ SOURCE_CONFIRMING receipt to ``on_checkpoint`` and then to the bound store — both before the
+ first confirmation poll — and returns the typed result. A polling timeout is not a failure:
+ the pending receipt comes back with the signature and blockhash lifetime.
+ """
+
+ def __init__(self, module: Any, *, route: Any, recipient: str, amount_atomic: int, plan: Any,
+ build_result: Callable[[Any], R], store: "CheckpointStore | None" = None) -> None:
+ self._module = module
+ self.route = route
+ self.recipient = recipient
+ self.amount_atomic = amount_atomic
+ self.plan = plan
+ self._build_result = build_result
+ self._store = store
+ self.quote: Any = None
+ self._built: Any = None
+ self._broadcast_id: str | None = None
+
+ def _record_broadcast(self, signature: str) -> None:
+ """Arm the single-use guard with the signature this call put on the wire (or may have)."""
+ if self._broadcast_id is None:
+ self._broadcast_id = signature
+
+ def build(self) -> Any:
+ """Partially signed ``VersionedTransaction`` (unique-message signer only); sets ``self.quote``."""
+ self._built = self._module._build_transaction(recipient=self.recipient,
+ amount_atomic=self.amount_atomic, plan=self.plan)
+ self.quote = self._built.quote
+ return self._built.transaction
+
+ def send(self, *, wait: bool = True, timeout_seconds: float = 120.0, poll_seconds: float = 1.0,
+ on_checkpoint: Callable[[Any], None] | None = None) -> R:
+ """Broadcast once. A call is single-use from the moment its transaction reaches the node (or
+ may have, when the RPC response was lost): a second ``send()`` raises rather than signing a
+ second transfer of the same funds. A failure before the broadcast — a stale plan, an
+ insufficient balance — leaves the call usable, and ``build()`` stays repeatable."""
+ if self._broadcast_id is not None:
+ raise BridgeError(
+ f"this call already broadcast {self._broadcast_id}; use bridge.sol.source_status(plan, receipt) "
+ "to follow it — do not resend")
+ self.build()
+ receipt = self._module._submit(self._built, wait=wait, timeout_seconds=timeout_seconds,
+ poll_seconds=poll_seconds, on_checkpoint=on_checkpoint,
+ store=self._store, on_broadcast=self._record_broadcast)
+ return self._build_result(receipt)
+
+
+__all__ = ["AleoCall", "EvmCall", "EvmOutcome", "EvmStep", "SolCall", "extract_tx_id", "is_duplicate_submission",
+ "output_values", "payload_transitions", "root_outputs"]
diff --git a/bridge-sdk/python/aleo_bridge/_evm_abi.py b/bridge-sdk/python/aleo_bridge/_evm_abi.py
new file mode 100644
index 0000000..5fdb7fc
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_evm_abi.py
@@ -0,0 +1,60 @@
+"""Minimal ABI fragments for the reviewed Ethereum deployments (brief §3.1, §3.2, §3.7). Pure data."""
+from __future__ import annotations
+
+ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
+
+# The single EVM chain the bridge drives per environment (base/hyperevm carry only metadata-required routes).
+EVM_CHAIN_BY_ENVIRONMENT = {"mainnet": "ethereum", "testnet": "sepolia"}
+
+ERC20_ABI = [
+ {"type": "function", "name": "balanceOf", "stateMutability": "view",
+ "inputs": [{"name": "owner", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
+ {"type": "function", "name": "allowance", "stateMutability": "view",
+ "inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}],
+ "outputs": [{"name": "", "type": "uint256"}]},
+ {"type": "function", "name": "approve", "stateMutability": "nonpayable",
+ "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}],
+ "outputs": [{"name": "", "type": "bool"}]},
+]
+
+WARP_ROUTE_ABI = [
+ {"type": "function", "name": "quoteTransferRemote", "stateMutability": "view",
+ "inputs": [{"name": "destination", "type": "uint32"}, {"name": "recipient", "type": "bytes32"},
+ {"name": "amount", "type": "uint256"}],
+ "outputs": [{"name": "quotes", "type": "tuple[]",
+ "components": [{"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"}]}]},
+ {"type": "function", "name": "transferRemote", "stateMutability": "payable",
+ "inputs": [{"name": "destination", "type": "uint32"}, {"name": "recipient", "type": "bytes32"},
+ {"name": "amount", "type": "uint256"}],
+ "outputs": [{"name": "messageId", "type": "bytes32"}]},
+ {"type": "event", "name": "SentTransferRemote", "anonymous": False,
+ "inputs": [{"name": "destination", "type": "uint32", "indexed": True},
+ {"name": "recipient", "type": "bytes32", "indexed": True},
+ {"name": "amount", "type": "uint256", "indexed": False}]},
+]
+
+XRESERVE_ABI = [
+ {"type": "function", "name": "depositToRemote", "stateMutability": "nonpayable",
+ "inputs": [{"name": "value", "type": "uint256"}, {"name": "remoteDomain", "type": "uint32"},
+ {"name": "remoteRecipient", "type": "bytes32"}, {"name": "localToken", "type": "address"},
+ {"name": "maxFee", "type": "uint256"}, {"name": "hookData", "type": "bytes"}],
+ "outputs": []},
+ {"type": "event", "name": "DepositedToRemote", "anonymous": False,
+ "inputs": [{"name": "localToken", "type": "address", "indexed": True},
+ {"name": "value", "type": "uint256", "indexed": False},
+ {"name": "localDepositor", "type": "address", "indexed": True},
+ {"name": "remoteRecipient", "type": "bytes32", "indexed": True},
+ {"name": "remoteDomain", "type": "uint32", "indexed": False},
+ {"name": "remoteToken", "type": "bytes32", "indexed": False},
+ {"name": "maxFee", "type": "uint256", "indexed": False},
+ {"name": "hookData", "type": "bytes", "indexed": False}]},
+]
+
+MAILBOX_ABI = [
+ {"type": "event", "name": "DispatchId", "anonymous": False,
+ "inputs": [{"name": "messageId", "type": "bytes32", "indexed": True}]},
+ {"type": "function", "name": "delivered", "stateMutability": "view",
+ "inputs": [{"name": "id", "type": "bytes32"}], "outputs": [{"name": "", "type": "bool"}]},
+]
+
+__all__ = ["ERC20_ABI", "EVM_CHAIN_BY_ENVIRONMENT", "MAILBOX_ABI", "WARP_ROUTE_ABI", "XRESERVE_ABI", "ZERO_ADDRESS"]
diff --git a/bridge-sdk/python/aleo_bridge/_keccak.py b/bridge-sdk/python/aleo_bridge/_keccak.py
new file mode 100644
index 0000000..41bf250
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_keccak.py
@@ -0,0 +1,57 @@
+"""Pure-Python Keccak-256 (Ethereum's keccak; padding byte 0x01, NOT SHA3-256's 0x06).
+
+Vendored so an Aleo-only install can hash xReserve payloads, verify Circle attestations and
+checksum EVM addresses without web3. When web3 is installed the two agree (tested).
+"""
+from __future__ import annotations
+
+_RC = [
+ 0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
+ 0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
+ 0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
+ 0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
+ 0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
+ 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
+]
+_ROT = [[0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61],
+ [28, 55, 25, 21, 56], [27, 20, 39, 8, 14]]
+_MASK = (1 << 64) - 1
+_RATE = 136 # 1088-bit rate for a 256-bit digest
+
+
+def _rol(x: int, n: int) -> int:
+ n %= 64
+ return ((x << n) | (x >> (64 - n))) & _MASK if n else x
+
+
+def _keccak_f(a: list[int]) -> None:
+ for rc in _RC:
+ c = [a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20] for x in range(5)]
+ d = [c[(x - 1) % 5] ^ _rol(c[(x + 1) % 5], 1) for x in range(5)]
+ for i in range(25):
+ a[i] ^= d[i % 5]
+ b = [0] * 25
+ for x in range(5):
+ for y in range(5):
+ b[y + 5 * ((2 * x + 3 * y) % 5)] = _rol(a[x + 5 * y], _ROT[x][y])
+ for x in range(5):
+ for y in range(5):
+ a[x + 5 * y] = b[x + 5 * y] ^ ((~b[(x + 1) % 5 + 5 * y]) & b[(x + 2) % 5 + 5 * y])
+ a[0] ^= rc
+
+
+def keccak256(data: "bytes | bytearray | memoryview") -> bytes:
+ """Keccak-256 digest of *data* (32 bytes)."""
+ state = [0] * 25
+ msg = bytearray(data) + b"\x01"
+ msg += bytes((-len(msg)) % _RATE)
+ msg[-1] |= 0x80
+ for off in range(0, len(msg), _RATE):
+ block = msg[off:off + _RATE]
+ for i in range(_RATE // 8):
+ state[i] ^= int.from_bytes(block[8 * i:8 * i + 8], "little")
+ _keccak_f(state)
+ return b"".join(state[i].to_bytes(8, "little") for i in range(4))
+
+
+__all__ = ["keccak256"]
diff --git a/bridge-sdk/python/aleo_bridge/_plan.py b/bridge-sdk/python/aleo_bridge/_plan.py
new file mode 100644
index 0000000..98b4610
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_plan.py
@@ -0,0 +1,68 @@
+"""``build_plan`` — the one Plan builder every origin chain shares (brief §2.1, mirrors veil ``prepare``).
+
+Plan 4's ``lifecycle.prepare`` is the Tier-1 entry point; this helper is the Tier-2 path, so
+``bridge.eth.*`` / ``bridge.sol.*`` calls carry a checkpointable plan without importing lifecycle.
+It lives outside ``eth.py`` because the step shape is protocol- and chain-driven, not Ethereum-driven:
+the wallet steps' executor comes from the source chain's family, so the same builder describes an
+Ethereum, Solana or Aleo origin.
+"""
+from __future__ import annotations
+
+from .errors import BridgeError, UnsupportedRouteError
+from .registry import Asset, Registry, Route
+from .types import Plan, Step
+from .units import format_decimal_amount
+
+WALLET_EXECUTOR_BY_FAMILY = {"evm": "evm-wallet", "solana": "solana-wallet", "aleo": "aleo-wallet"}
+
+
+def _wallet_executor(registry: Registry, source: Asset) -> str:
+ """Who signs the source-chain steps: the wallet of the chain the funds leave from."""
+ family = registry.chain(source.chain_id).family
+ executor = WALLET_EXECUTOR_BY_FAMILY.get(family)
+ if executor is None:
+ raise BridgeError(f"No wallet executor for chain family {family!r} ({source.chain_id})")
+ return executor
+
+
+def build_plan(registry: Registry, route: Route, *, amount_atomic: int, recipient: str, sender: str | None,
+ mint_mode: str = "public") -> Plan:
+ """Build the ``Plan`` for one bridge route (mirrors veil ``prepare`` steps, brief §2.1)."""
+ source: Asset = registry.asset(route.source_asset_id)
+ destination: Asset = registry.asset(route.destination_asset_id)
+ if mint_mode not in ("public", "record", "private"):
+ raise BridgeError(f"mint_mode must be public, record or private; got {mint_mode!r}")
+ if mint_mode != "public" and route.protocol != "xreserve":
+ raise BridgeError("mint_mode other than public applies only to xReserve deposits to Aleo")
+ if amount_atomic <= 0:
+ raise BridgeError("amount_atomic must be positive")
+ wallet = _wallet_executor(registry, source)
+ source_family = registry.chain(source.chain_id).family
+ destination_family = registry.chain(destination.chain_id).family
+ if route.protocol == "xreserve":
+ if source_family == "evm" and destination_family == "aleo":
+ steps = (Step("source-approval", "approve", wallet, False),
+ Step("source-deposit", "deposit", wallet, True),
+ Step("deposit-attestation", "wait-attestation", "protocol", False),
+ Step("destination-mint", "mint", "aleo-wallet" if mint_mode == "private" else "protocol", False))
+ elif source_family == "aleo" and destination_family == "evm":
+ steps = (Step("source-burn", "burn", wallet, True),
+ Step("withdrawal-attestation", "wait-attestation", "protocol", False),
+ Step("destination-withdrawal", "withdraw", "protocol", False),
+ Step("destination-confirmation", "confirm-delivery", "protocol", False))
+ else:
+ raise UnsupportedRouteError(f"Unsupported xReserve route direction: {route.id}")
+ else:
+ # Aleo ARC-20 tokens need no on-chain approval; only a non-Aleo token source does.
+ needs_approval = source.kind == "token" and source_family != "aleo"
+ steps = tuple([Step("source-approval", "approve", wallet, False)] if needs_approval else []) + (
+ Step("source-dispatch", "dispatch", wallet, True),
+ Step("message-delivery", "wait-delivery", "protocol", False),
+ Step("destination-confirmation", "confirm-delivery", "protocol", False))
+ return Plan(route_id=route.id, registry_version=registry.version, protocol=route.protocol,
+ environment=route.environment, source_asset_id=source.id, destination_asset_id=destination.id,
+ amount=format_decimal_amount(amount_atomic, source.decimals), amount_atomic=amount_atomic,
+ recipient=recipient, sender=sender, mint_mode=mint_mode, steps=steps)
+
+
+__all__ = ["WALLET_EXECUTOR_BY_FAMILY", "build_plan"]
diff --git a/bridge-sdk/python/aleo_bridge/_registry_data.py b/bridge-sdk/python/aleo_bridge/_registry_data.py
new file mode 100644
index 0000000..3bf6c1e
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_registry_data.py
@@ -0,0 +1,386 @@
+"""Verbatim port of veil ``packages/bridge/src/registry/default.ts`` (commit 8d62198, registry version
+2026-08-31.solana-deposits.1). Plain dicts only — ``registry.py`` turns them into dataclasses.
+Every key keeps veil's camelCase spelling so the brief and veil tests stay the source of truth."""
+from __future__ import annotations
+
+REGISTRY_VERSION = "2026-08-31.solana-deposits.1"
+
+EVM_ADDRESS = "^0x[0-9a-fA-F]{40}$"
+SOLANA_ADDRESS = "^[1-9A-HJ-NP-Za-km-z]{32,44}$"
+ALEO_ADDRESS = "^aleo1[0-9a-z]{58}$"
+
+CHAINS = [
+ {"id": "aleo", "displayName": "Aleo", "family": "aleo", "environment": "mainnet", "nativeCurrencySymbol": "ALEO",
+ "protocolDomains": {"xreserve": 10002, "hyperlane": 1634493807}},
+ {"id": "ethereum", "displayName": "Ethereum", "family": "evm", "environment": "mainnet", "nativeCurrencySymbol": "ETH",
+ "protocolDomains": {"xreserve": 0, "hyperlane": 1}},
+ {"id": "solana", "displayName": "Solana", "family": "solana", "environment": "mainnet", "nativeCurrencySymbol": "SOL",
+ "protocolDomains": {"hyperlane": 1399811149}},
+ {"id": "base", "displayName": "Base", "family": "evm", "environment": "mainnet", "nativeCurrencySymbol": "ETH"},
+ {"id": "hyperevm", "displayName": "HyperEVM", "family": "evm", "environment": "mainnet", "nativeCurrencySymbol": "HYPE"},
+ {"id": "aleo-testnet", "displayName": "Aleo Testnet", "family": "aleo", "environment": "testnet", "nativeCurrencySymbol": "ALEO",
+ "protocolDomains": {"xreserve": 10002, "hyperlane": 1617853565}},
+ {"id": "sepolia", "displayName": "Ethereum Sepolia", "family": "evm", "environment": "testnet", "nativeCurrencySymbol": "ETH",
+ "protocolDomains": {"hyperlane": 11155111}},
+]
+
+ASSETS = [
+ {"id": "aleo/aleo", "key": "aleo", "chainId": "aleo", "symbol": "ALEO", "name": "Aleo", "decimals": 6, "kind": "native",
+ "locator": {"kind": "aleo-program", "value": "credits.aleo"}, "addressValidationRegex": ALEO_ADDRESS},
+ {"id": "aleo/usdcx", "key": "usdcx", "chainId": "aleo", "symbol": "USDCx", "name": "USDCx", "decimals": 6, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "usdcx_stablecoin.aleo"}, "addressValidationRegex": ALEO_ADDRESS,
+ "privacy": {"kind": "arc22", "program": "usdcx_stablecoin.aleo"}},
+ {"id": "aleo/eth", "key": "eth", "chainId": "aleo", "symbol": "ETH", "name": "Hyperlane ETH", "decimals": 18, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "hyp_warp_token_eth_v2.aleo",
+ "tokenId": "aleo1t7f29tq9qng2lfvrkpcuvu59jn24hrmzqdyqfn6p0u5p80npfvqqecmkj8"},
+ "addressValidationRegex": ALEO_ADDRESS, "privacy": {"kind": "arc20", "program": "arc20_eth.aleo"}},
+ {"id": "aleo/wbtc", "key": "wbtc", "chainId": "aleo", "symbol": "WBTC", "name": "Hyperlane WBTC", "decimals": 8, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "hyp_warp_token_wbtc_v2.aleo",
+ "tokenId": "aleo1240fsvz2dhmj0cdtt8mc0yc8um9fmu236rqcl2qnlj9703hd2vpsdwyrtf"},
+ "addressValidationRegex": ALEO_ADDRESS, "privacy": {"kind": "arc20", "program": "arc20_wbtc.aleo"}},
+ {"id": "aleo/usdt", "key": "usdt", "chainId": "aleo", "symbol": "USDT", "name": "Hyperlane USDT", "decimals": 6, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "hyp_warp_token_usdt_v2.aleo",
+ "tokenId": "aleo18yynfz0lrfx0tund540vy2z7gju7ekgqsueg5jgu28mpm2z42ufq7qua8y"},
+ "addressValidationRegex": ALEO_ADDRESS, "privacy": {"kind": "arc20", "program": "arc20_usdt.aleo"}},
+ {"id": "aleo/sol", "key": "sol", "chainId": "aleo", "symbol": "SOL", "name": "Hyperlane SOL", "decimals": 9, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "hyp_warp_token_sol_v2.aleo",
+ "tokenId": "aleo1aa0zt0vg9uwknekpqeefkvad55swp7833wc5crp2prv0lm4djuxs5r7k6v"},
+ "addressValidationRegex": ALEO_ADDRESS, "privacy": {"kind": "arc20", "program": "arc20_sol.aleo"}},
+ {"id": "aleo/usad", "key": "usad", "chainId": "aleo", "symbol": "USAD", "name": "USAD", "decimals": 6, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "usad_stablecoin.aleo"}, "addressValidationRegex": ALEO_ADDRESS},
+ {"id": "ethereum/usdc", "key": "usdc", "chainId": "ethereum", "symbol": "USDC", "name": "USD Coin", "decimals": 6, "kind": "token",
+ "locator": {"kind": "evm-contract", "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}, "addressValidationRegex": EVM_ADDRESS},
+ {"id": "ethereum/eth", "key": "eth", "chainId": "ethereum", "symbol": "ETH", "name": "Ether", "decimals": 18, "kind": "native",
+ "locator": {"kind": "native", "value": "ETH"}, "addressValidationRegex": EVM_ADDRESS},
+ {"id": "ethereum/wbtc", "key": "wbtc", "chainId": "ethereum", "symbol": "WBTC", "name": "Wrapped Bitcoin", "decimals": 8, "kind": "token",
+ "locator": {"kind": "evm-contract", "value": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"}, "addressValidationRegex": EVM_ADDRESS},
+ {"id": "ethereum/usdt", "key": "usdt", "chainId": "ethereum", "symbol": "USDT", "name": "Tether USD", "decimals": 6, "kind": "token",
+ "locator": {"kind": "evm-contract", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7"}, "addressValidationRegex": EVM_ADDRESS},
+ {"id": "ethereum/aleo", "key": "aleo", "chainId": "ethereum", "symbol": "ALEO", "name": "Hyperlane ALEO", "decimals": 6, "kind": "token",
+ "addressValidationRegex": EVM_ADDRESS},
+ {"id": "ethereum/usad", "key": "usad", "chainId": "ethereum", "symbol": "USAD", "name": "USAD route collateral", "decimals": 6, "kind": "token",
+ "addressValidationRegex": EVM_ADDRESS},
+ {"id": "solana/sol", "key": "sol", "chainId": "solana", "symbol": "SOL", "name": "Solana", "decimals": 9, "kind": "native",
+ "locator": {"kind": "native", "value": "SOL"}, "addressValidationRegex": SOLANA_ADDRESS},
+ {"id": "solana/aleo", "key": "aleo", "chainId": "solana", "symbol": "ALEO", "name": "Hyperlane ALEO", "decimals": 6, "kind": "token",
+ "addressValidationRegex": SOLANA_ADDRESS},
+ {"id": "base/aleo", "key": "aleo", "chainId": "base", "symbol": "ALEO", "name": "Hyperlane ALEO", "decimals": 6, "kind": "token",
+ "addressValidationRegex": EVM_ADDRESS},
+ {"id": "hyperevm/aleo", "key": "aleo", "chainId": "hyperevm", "symbol": "ALEO", "name": "Hyperlane ALEO", "decimals": 6, "kind": "token",
+ "addressValidationRegex": EVM_ADDRESS},
+ {"id": "aleo-testnet/usdcx", "key": "usdcx", "chainId": "aleo-testnet", "symbol": "USDCx", "name": "Testnet USDCx", "decimals": 6, "kind": "token",
+ "locator": {"kind": "aleo-program", "value": "test_usdcx_stablecoin.aleo"}, "addressValidationRegex": ALEO_ADDRESS,
+ "privacy": {"kind": "arc22", "program": "test_usdcx_stablecoin.aleo"}},
+ {"id": "sepolia/usdc", "key": "usdc", "chainId": "sepolia", "symbol": "USDC", "name": "Testnet USD Coin", "decimals": 6, "kind": "token",
+ "locator": {"kind": "evm-contract", "value": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"}, "addressValidationRegex": EVM_ADDRESS},
+]
+
+XRESERVE_SOURCE = "https://developers.circle.com/xreserve/references/supported-blockchains-and-domains"
+HYPERLANE_REGISTRY_COMMIT = "2621c16f2db1ccb46643265c110dac5ca2c7c51a"
+HYPERLANE_SOURCE = f"https://github.com/hyperlane-xyz/hyperlane-registry/tree/{HYPERLANE_REGISTRY_COMMIT}/deployments/warp_routes"
+
+ALEO_USDT_HYPERLANE_CONFIG_SOURCE = "https://github.com/hyperlane-xyz/hyperlane-registry/blob/418056e21734d26a7d14692e0ec5e902cc9e86bf/deployments/warp_routes/USDT/aleo-config.yaml"
+ALEO_SOL_HYPERLANE_CONFIG_SOURCE = "https://github.com/hyperlane-xyz/hyperlane-registry/blob/418056e21734d26a7d14692e0ec5e902cc9e86bf/deployments/warp_routes/SOL/aleo-config.yaml"
+
+ETHEREUM_HYPERLANE_COMMON = {
+ "sourceChainId": 1,
+ "destinationDomain": 1634493807,
+ "mailboxAddress": "0xc005dc82818d67AF737725bD4bf75435d065D239",
+ "interchainGasPaymaster": "0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611",
+ "interchainSecurityModule": "0x0000000000000000000000000000000000000000",
+ "registryCommit": HYPERLANE_REGISTRY_COMMIT,
+}
+ETH_HYPERLANE_METADATA = {
+ **ETHEREUM_HYPERLANE_COMMON,
+ "routerAddress": "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A",
+ "routerType": "native",
+ "destinationRouter": "hyp_warp_token_eth_v2.aleo/aleo1t7f29tq9qng2lfvrkpcuvu59jn24hrmzqdyqfn6p0u5p80npfvqqecmkj8",
+}
+WBTC_HYPERLANE_METADATA = {
+ **ETHEREUM_HYPERLANE_COMMON,
+ "routerAddress": "0x20CDC85778b732073F7EecEF3DF25c0d310f8772",
+ "routerType": "collateral",
+ "tokenAddress": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
+ "destinationRouter": "hyp_warp_token_wbtc_v2.aleo/aleo1240fsvz2dhmj0cdtt8mc0yc8um9fmu236rqcl2qnlj9703hd2vpsdwyrtf",
+}
+USDT_HYPERLANE_METADATA = {
+ **ETHEREUM_HYPERLANE_COMMON,
+ "routerAddress": "0x3C2064D78e4578E8F936E3db42aEF044E33FBF31",
+ "routerType": "collateral",
+ "tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
+ "destinationRouter": "hyp_warp_token_usdt_v2.aleo/aleo18yynfz0lrfx0tund540vy2z7gju7ekgqsueg5jgu28mpm2z42ufq7qua8y",
+ "requiresApprovalReset": True,
+}
+
+# Intentionally non-live values that only expose the transfer_remote ABI; execution refuses them.
+ALEO_PLACEHOLDER_ADDRESS = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ALEO_PLACEHOLDER_BYTES32 = "[" + ", ".join(["0u8"] * 32) + "]"
+ZERO_ADDRESS = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
+IGP_HOOK = "aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74"
+
+ALEO_MAILBOX_METADATA = {
+ "aleoMailboxStateVerified": True,
+ "aleoHookManagerProgram": "hyp_hook_manager.aleo",
+ "aleoHookManagerProgramSource": "https://explorer.provable.com/program/hyp_hook_manager.aleo",
+ "aleoMailboxProgram": "hyp_mailbox.aleo",
+ "aleoMailboxProgramEdition": 0,
+ "aleoMailboxProgramSource": "https://explorer.provable.com/program/hyp_mailbox.aleo",
+ "aleoMailboxMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_mailbox.aleo/mapping/mailbox/true",
+ "aleoMailboxMetadataReviewedAt": "2026-08-17",
+ "aleoMailboxLocalDomain": 1634493807,
+ "aleoMailboxObservedNonce": 170,
+ "aleoMailboxObservedProcessCount": 291,
+ "aleoMailboxDefaultIsm": "aleo1yvf5kcsdgnescqq2lar83mms79yh3ugvc3y0mdnlgvx4lyh5zugqr9hptk",
+ "aleoMailboxDefaultHook": IGP_HOOK,
+ "aleoMailboxRequiredHook": "aleo1yxevh9qgxehej46j7vueplwjcpfdfml2dje3ey4ukzknx7wzasgqnxgq82",
+ "aleoMailboxDispatchProxy": "aleo1sge9kmjzs3d8fqrscy4hwn7vf9vw4jcxe877lv0m2w8hay78lsxsqg975s",
+ "aleoMailboxOwner": "aleo1ypf8xgvz560ukw25hufj3d77gx69pdcy70nssdfdxd97j80d7cqs98d7x8",
+}
+
+
+def _aleo_hyperlane_placeholders(program: str, destination_domain: int) -> dict:
+ return {
+ "aleoRouterProgram": program,
+ "aleoDestinationDomain": destination_domain,
+ "aleoPlaceholderConfiguration": True,
+ "aleoTokenType": "0",
+ "aleoTokenOwner": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoIsm": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoHook": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoTokenId": "0field",
+ "aleoRemoteRouterRecipient": ALEO_PLACEHOLDER_BYTES32,
+ "aleoRemoteRouterGas": "0",
+ "aleoRecipient": "[0u128, 0u128]",
+ "aleoAllowanceSpender0": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoAllowanceAmount0": "0",
+ "aleoAllowanceSpender1": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoAllowanceAmount1": "0",
+ "aleoAllowanceSpender2": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoAllowanceAmount2": "0",
+ "aleoAllowanceSpender3": ALEO_PLACEHOLDER_ADDRESS,
+ "aleoAllowanceAmount3": "0",
+ **ALEO_MAILBOX_METADATA,
+ }
+
+
+ALEO_WBTC_APP_METADATA = {
+ "aleoAppMetadataVerified": True,
+ "aleoProgramSource": "https://explorer.provable.com/program/hyp_warp_token_wbtc_v2.aleo",
+ "aleoAppMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_wbtc_v2.aleo/mapping/app_metadata/true",
+ "aleoAppMetadataReviewedAt": "2026-08-17",
+ "aleoProgramEdition": 0,
+ "aleoTokenType": "1",
+ "aleoTokenOwner": "aleo14jauje2a5sncm9u5t3mt6qqv3eq2hatkddskccs0dvsy35a0x58q0d6f95",
+ "aleoIsm": ZERO_ADDRESS,
+ "aleoHook": ZERO_ADDRESS,
+ "aleoTokenId": "1505227928464760254508513036497943623956572091841806589002910775534260084309field",
+ "aleoLocalDecimals": 8,
+ "aleoRemoteDecimals": 8,
+}
+ALEO_ETH_APP_METADATA = {
+ "aleoAppMetadataVerified": True,
+ "aleoProgramSource": "https://explorer.provable.com/program/hyp_warp_token_eth_v2.aleo",
+ "aleoAppMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_eth_v2.aleo/mapping/app_metadata/true",
+ "aleoAppMetadataReviewedAt": "2026-08-17",
+ "aleoProgramEdition": 0,
+ "aleoTokenType": "1",
+ "aleoTokenOwner": "aleo1wq6f6qdqya44avznygz5hae40u3mjg64w0r93a4qfu4utpf8cg9q566f4r",
+ "aleoIsm": ZERO_ADDRESS,
+ "aleoHook": ZERO_ADDRESS,
+ "aleoTokenId": "133188123661477349522757068766864658505569365361420630212878794317749195359field",
+ "aleoLocalDecimals": 18,
+ "aleoRemoteDecimals": 18,
+}
+ALEO_USDT_APP_METADATA = {
+ "aleoAppMetadataVerified": True,
+ "aleoProgramSource": "https://explorer.provable.com/program/hyp_warp_token_usdt_v2.aleo",
+ "aleoAppMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_usdt_v2.aleo/mapping/app_metadata/true",
+ "aleoAppMetadataReviewedAt": "2026-08-17",
+ "aleoProgramEdition": 1,
+ "aleoTokenType": "1",
+ "aleoTokenOwner": "aleo1l3gwacmjruxryy9c7c4fn0acyzprf29hucrvthw7f63lpyhd5y9srydq8z",
+ "aleoIsm": ZERO_ADDRESS,
+ "aleoHook": ZERO_ADDRESS,
+ "aleoTokenId": "8295938150000417034830036849466229528602563851235385582732969109393809606969field",
+ "aleoLocalDecimals": 6,
+ "aleoRemoteDecimals": 18,
+ "aleoScale": "1000000000000",
+ "aleoHyperlaneConfigSource": ALEO_USDT_HYPERLANE_CONFIG_SOURCE,
+}
+ALEO_SOL_APP_METADATA = {
+ "aleoAppMetadataVerified": True,
+ "aleoProgramSource": "https://explorer.provable.com/program/hyp_warp_token_sol_v2.aleo",
+ "aleoAppMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_sol_v2.aleo/mapping/app_metadata/true",
+ "aleoAppMetadataReviewedAt": "2026-08-17",
+ "aleoProgramEdition": 0,
+ "aleoTokenType": "1",
+ "aleoTokenOwner": "aleo1wr8rfr4ggedjxtg5e23s38zqkgy2j05uc9l8t4akjp5zcw3levpswkwk45",
+ "aleoIsm": ZERO_ADDRESS,
+ "aleoHook": ZERO_ADDRESS,
+ "aleoTokenId": "6148061383892805373029428966764338809222769879628268522058032128225601478383field",
+ "aleoLocalDecimals": 9,
+ "aleoRemoteDecimals": 9,
+ "aleoHyperlaneConfigSource": ALEO_SOL_HYPERLANE_CONFIG_SOURCE,
+}
+
+_ALLOWANCES = {
+ "aleoAllowanceSpendersVerified": True,
+ "aleoUnusedAllowancesVerified": True,
+ "aleoAllowanceSpender0": IGP_HOOK,
+ "aleoAllowanceSpender1": ZERO_ADDRESS,
+ "aleoAllowanceSpender2": ZERO_ADDRESS,
+ "aleoAllowanceSpender3": ZERO_ADDRESS,
+ "aleoAllowanceAmount1": "0",
+ "aleoAllowanceAmount2": "0",
+ "aleoAllowanceAmount3": "0",
+}
+ALEO_ETH_REMOTE_ROUTER = {
+ "aleoRemoteRouterVerified": True,
+ "aleoRemoteRouterSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_eth_v2.aleo/mapping/remote_routers/1u32",
+ "aleoRemoteRouterReviewedAt": "2026-08-17",
+ "aleoSampleTransferSource": "https://explorer.provable.com/transaction/at1vu0yckkms887zkl3qz7plnncd56jtf5zeal4uj2808upsjkusy8q7yp9v8",
+ "aleoDestinationDomain": 1,
+ "aleoRemoteRouterEvmAddress": "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A",
+ "aleoRemoteRouterRecipient": "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 56u8, 212u8, 71u8, 105u8, 79u8, 92u8, 31u8, 119u8, 58u8, 227u8, 19u8, 44u8, 249u8, 59u8, 243u8, 11u8, 126u8, 193u8, 250u8, 90u8]",
+ "aleoRemoteRouterGas": "44000",
+ **_ALLOWANCES,
+}
+ALEO_WBTC_REMOTE_ROUTER = {
+ "aleoRemoteRouterVerified": True,
+ "aleoRemoteRouterSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_wbtc_v2.aleo/mapping/remote_routers/1u32",
+ "aleoRemoteRouterReviewedAt": "2026-08-17",
+ "aleoDestinationDomain": 1,
+ "aleoRemoteRouterEvmAddress": "0x20CDC85778b732073F7EecEF3DF25c0d310f8772",
+ "aleoRemoteRouterRecipient": "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 32u8, 205u8, 200u8, 87u8, 120u8, 183u8, 50u8, 7u8, 63u8, 126u8, 236u8, 239u8, 61u8, 242u8, 92u8, 13u8, 49u8, 15u8, 135u8, 114u8]",
+ "aleoRemoteRouterGas": "68000",
+ **_ALLOWANCES,
+}
+ALEO_USDT_ETHEREUM_REMOTE_ROUTER = {
+ "aleoRemoteRouterVerified": True,
+ "aleoRemoteRouterSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_usdt_v2.aleo/mapping/remote_routers/1u32",
+ "aleoRemoteRouterReviewedAt": "2026-08-17",
+ "aleoSampleTransferSource": "https://explorer.provable.com/transaction/at19caeeee8v3xc4kfwen4tx89f0tnggrpjp0anrhq2ca3y82xr9q8qyz8a9r",
+ "aleoSampleTransferDestinationDomain": 56,
+ "aleoDestinationDomain": 1,
+ "aleoRemoteRouterEvmAddress": "0x3C2064D78e4578E8F936E3db42aEF044E33FBF31",
+ "aleoRemoteRouterRecipient": "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 60u8, 32u8, 100u8, 215u8, 142u8, 69u8, 120u8, 232u8, 249u8, 54u8, 227u8, 219u8, 66u8, 174u8, 240u8, 68u8, 227u8, 63u8, 191u8, 49u8]",
+ "aleoRemoteRouterGas": "68000",
+ **_ALLOWANCES,
+}
+ALEO_SOL_REMOTE_ROUTER = {
+ "aleoRemoteRouterVerified": True,
+ "aleoRemoteRouterSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_sol_v2.aleo/mapping/remote_routers/1399811149u32",
+ "aleoRemoteRouterReviewedAt": "2026-08-17",
+ "aleoSampleTransitionId": "au15fg39h53h55tkj0nexrme3k6pvgxngxapcyajdhf06jcg3cyeugq5kd7hg",
+ "aleoDestinationDomain": 1399811149,
+ "aleoRemoteRouterSolanaAddress": "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7",
+ "aleoRemoteRouterRecipient": "[112u8, 4u8, 72u8, 22u8, 219u8, 143u8, 68u8, 202u8, 21u8, 197u8, 236u8, 182u8, 198u8, 142u8, 52u8, 96u8, 142u8, 38u8, 51u8, 113u8, 116u8, 143u8, 96u8, 123u8, 104u8, 126u8, 97u8, 73u8, 7u8, 6u8, 211u8, 122u8]",
+ "aleoRemoteRouterGas": "300000",
+ **_ALLOWANCES,
+}
+ALEO_WITHDRAWAL_ACTIVATION = {"aleoPlaceholderConfiguration": False, "aleoWithdrawalReviewedAt": "2026-08-26"}
+
+SOLANA_SOL_DEPOSIT_METADATA = {
+ "warpProgramAddress": "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7",
+ "tokenPda": "JDkpV5CsSbhyGhHhirC5DjGPTcuKWUVHtBZ5MFsgu3ZW",
+ "nativeCollateralPda": "8HY3hxmnrWwqEmcdwkSnfN9wEQFUkyiwZvU1vMbnXgbC",
+ "dispatchAuthorityPda": "ATDttjggAZKyS19kcV6Rn56oMi49gDprZGckRou9vkkY",
+ "mailboxProgramAddress": "E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi",
+ "mailboxOutboxPda": "BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
+ "igpProgramAddress": "BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv",
+ "igpProgramDataPda": "8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho",
+ "igpAccount": "JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M",
+ "igpOverheadAccount": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF",
+ "splNoopProgramAddress": "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
+ "destinationDomain": 1634493807,
+ "destinationGasAmount": "464000",
+ "registryCommit": "418056e21734d26a7d14692e0ec5e902cc9e86bf",
+ "solanaReviewedAt": "2026-08-31",
+ "solanaConfigSource": ALEO_SOL_HYPERLANE_CONFIG_SOURCE,
+}
+
+XRESERVE_MAINNET_METADATA = {
+ "xReserveContract": "0x8888888199b2Df864bf678259607d6D5EBb4e3Ce",
+ "sourceChainId": 1,
+ "sourceDomain": 0,
+ "ethereumDestinationDomain": 0,
+ "arcDestinationDomain": 26,
+ "remoteDomain": 10002,
+ "remoteToken": "usdcx_stablecoin.aleo",
+ "remoteTokenBytes32": "0x11ea7dab1d29d5f61500582c63e98c42e1165f9ba050ea9d0c6af9f871987711",
+ "minimumAmountAtomic": "2000000",
+ "withdrawalFeeAtomic": "2000000",
+ "maxFeeAtomic": "100000",
+ "bridgeProgram": "usdcx_bridge_v2.aleo",
+ "wrapperProgram": "shielded_usdcx_wrapper.aleo",
+ "attestationBaseUrl": "https://xreserve-api.circle.com/v1/attestations",
+}
+XRESERVE_TESTNET_METADATA = {
+ "xReserveContract": "0x008888878f94C0d87defdf0B07f46B93C1934442",
+ "sourceChainId": 11155111,
+ "sourceDomain": 0,
+ "ethereumDestinationDomain": 0,
+ "arcDestinationDomain": 26,
+ "remoteDomain": 10002,
+ "remoteToken": "test_usdcx_stablecoin.aleo",
+ "remoteTokenBytes32": "0xb143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401",
+ "minimumAmountAtomic": "2000000",
+ "withdrawalFeeAtomic": "2000000",
+ "maxFeeAtomic": "100000",
+ "bridgeProgram": "test_usdcx_bridge_v2.aleo",
+ "wrapperProgram": "shielded_usdcx_wrapper.aleo",
+ "attestationBaseUrl": "https://xreserve-api-testnet.circle.com/v1/attestations",
+}
+
+
+def _route(id: str, protocol: str, environment: str, source_asset_id: str, destination_asset_id: str,
+ availability: str, deployment_id: str, metadata: dict) -> dict:
+ return {
+ "id": id, "protocol": protocol, "environment": environment,
+ "sourceAssetId": source_asset_id, "destinationAssetId": destination_asset_id,
+ "availability": availability, "deploymentId": deployment_id,
+ "source": XRESERVE_SOURCE if protocol == "xreserve" else HYPERLANE_SOURCE,
+ "metadata": dict(metadata),
+ }
+
+
+def _pair(protocol: str, environment: str, left: str, right: str, availability: str,
+ deployment_id: str, metadata: dict) -> list[dict]:
+ return [
+ _route(f"{protocol}:{left}->{right}", protocol, environment, left, right, availability, deployment_id, metadata),
+ _route(f"{protocol}:{right}->{left}", protocol, environment, right, left, availability, deployment_id, metadata),
+ ]
+
+
+ROUTES = [
+ *_pair("xreserve", "mainnet", "ethereum/usdc", "aleo/usdcx", "active", "xreserve-usdcx-aleo", XRESERVE_MAINNET_METADATA),
+ *_pair("xreserve", "testnet", "sepolia/usdc", "aleo-testnet/usdcx", "active", "xreserve-usdcx-aleo-testnet", XRESERVE_TESTNET_METADATA),
+ _route("hyperlane:ethereum/eth->aleo/eth", "hyperlane", "mainnet", "ethereum/eth", "aleo/eth", "active", "ETH/aleo",
+ {**ETH_HYPERLANE_METADATA, **ALEO_MAILBOX_METADATA}),
+ _route("hyperlane:aleo/eth->ethereum/eth", "hyperlane", "mainnet", "aleo/eth", "ethereum/eth", "active", "ETH/aleo",
+ {**ETH_HYPERLANE_METADATA, **_aleo_hyperlane_placeholders("hyp_warp_token_eth_v2.aleo", 1),
+ **ALEO_ETH_APP_METADATA, **ALEO_ETH_REMOTE_ROUTER, **ALEO_WITHDRAWAL_ACTIVATION}),
+ _route("hyperlane:ethereum/wbtc->aleo/wbtc", "hyperlane", "mainnet", "ethereum/wbtc", "aleo/wbtc", "active", "WBTC/aleo",
+ {**WBTC_HYPERLANE_METADATA, **ALEO_MAILBOX_METADATA}),
+ _route("hyperlane:aleo/wbtc->ethereum/wbtc", "hyperlane", "mainnet", "aleo/wbtc", "ethereum/wbtc", "active", "WBTC/aleo",
+ {**WBTC_HYPERLANE_METADATA, **_aleo_hyperlane_placeholders("hyp_warp_token_wbtc_v2.aleo", 1),
+ **ALEO_WBTC_APP_METADATA, **ALEO_WBTC_REMOTE_ROUTER, **ALEO_WITHDRAWAL_ACTIVATION}),
+ _route("hyperlane:ethereum/usdt->aleo/usdt", "hyperlane", "mainnet", "ethereum/usdt", "aleo/usdt", "active", "USDT/aleo",
+ {**USDT_HYPERLANE_METADATA, **ALEO_MAILBOX_METADATA}),
+ _route("hyperlane:aleo/usdt->ethereum/usdt", "hyperlane", "mainnet", "aleo/usdt", "ethereum/usdt", "active", "USDT/aleo",
+ {**USDT_HYPERLANE_METADATA, **_aleo_hyperlane_placeholders("hyp_warp_token_usdt_v2.aleo", 1),
+ **ALEO_USDT_APP_METADATA, **ALEO_USDT_ETHEREUM_REMOTE_ROUTER, **ALEO_WITHDRAWAL_ACTIVATION}),
+ _route("hyperlane:solana/sol->aleo/sol", "hyperlane", "mainnet", "solana/sol", "aleo/sol", "active", "SOL/aleo",
+ {**SOLANA_SOL_DEPOSIT_METADATA, **ALEO_MAILBOX_METADATA}),
+ _route("hyperlane:aleo/sol->solana/sol", "hyperlane", "mainnet", "aleo/sol", "solana/sol", "active", "SOL/aleo",
+ {**_aleo_hyperlane_placeholders("hyp_warp_token_sol_v2.aleo", 1399811149),
+ **ALEO_SOL_APP_METADATA, **ALEO_SOL_REMOTE_ROUTER, **ALEO_WITHDRAWAL_ACTIVATION}),
+ *_pair("hyperlane", "mainnet", "aleo/aleo", "ethereum/aleo", "metadata-required", "ALEO/aleo", ALEO_MAILBOX_METADATA),
+ *_pair("hyperlane", "mainnet", "aleo/aleo", "solana/aleo", "metadata-required", "ALEO/aleo", ALEO_MAILBOX_METADATA),
+ *_pair("hyperlane", "mainnet", "aleo/aleo", "base/aleo", "metadata-required", "ALEO/aleo", ALEO_MAILBOX_METADATA),
+ *_pair("hyperlane", "mainnet", "aleo/aleo", "hyperevm/aleo", "metadata-required", "ALEO/aleo", ALEO_MAILBOX_METADATA),
+ _route("hyperlane:ethereum/usad->aleo/usad", "hyperlane", "mainnet", "ethereum/usad", "aleo/usad", "metadata-required", "USAD/aleo",
+ ALEO_MAILBOX_METADATA),
+ _route("hyperlane:aleo/usad->ethereum/usad", "hyperlane", "mainnet", "aleo/usad", "ethereum/usad", "metadata-required", "USAD/aleo",
+ _aleo_hyperlane_placeholders("hyp_warp_token_usad_v2.aleo", 1)),
+]
diff --git a/bridge-sdk/python/aleo_bridge/_sealevel.py b/bridge-sdk/python/aleo_bridge/_sealevel.py
new file mode 100644
index 0000000..c89379e
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/_sealevel.py
@@ -0,0 +1,388 @@
+"""Pure Hyperlane sealevel (Solana) layouts for the SOL warp route.
+
+Nothing here imports solders or solana-py: inputs and outputs are ``bytes``,
+``int`` and base58 ``str`` so every layout is testable on an Aleo-only
+install. Sources: veil ``src/solana/SEALEVEL_NOTES.md`` (primary-source
+derivations against hyperlane-monorepo 45c0988), ``src/solana/transferRemote.ts``,
+``src/solana/igp.ts``, ``src/protocols/hyperlane/solanaMetadata.ts``, and the
+recorded mainnet deposit in ``tests/fixtures/sealevel-transfer-remote.json``.
+"""
+from __future__ import annotations
+
+import hashlib
+import re
+from dataclasses import dataclass
+from typing import Mapping, Sequence
+
+from ._base58 import b58decode, b58encode
+from .errors import BridgeError, ConfigurationError, InvalidAmountError, InvalidRecipientError, RouteUnavailableError
+from .registry import Route
+
+# SEALEVEL_NOTES §1: every Sealevel Hyperlane instruction is prefixed with this
+# fixed 8-byte discriminator; TransferRemote is Borsh enum variant 1.
+PROGRAM_INSTRUCTION_DISCRIMINATOR = bytes([1] * 8)
+TRANSFER_REMOTE_VARIANT_TAG = 1
+INSTRUCTION_DATA_BYTES = 77 # 8 + 1 + 4 + 32 + 32
+U256_BYTES = 32
+
+
+def build_transfer_remote_instruction_data(destination_domain: int, recipient32: bytes, amount: int) -> bytes:
+ """``[8B 0x01×8][1B 0x01][u32 LE domain][32B recipient][u256 LE amount]`` — 77 bytes.
+
+ ``recipient32`` is the raw bech32m payload from ``encoding.aleo_address_to_bytes32``
+ (no byte reversal); ``amount`` is lamports.
+ """
+ if not 0 <= destination_domain <= 0xFFFF_FFFF:
+ raise ConfigurationError(f"destination domain {destination_domain} does not fit in a u32")
+ if len(recipient32) != 32:
+ raise InvalidRecipientError(f"recipient must be exactly 32 bytes, got {len(recipient32)}")
+ if not 0 <= amount < (1 << (U256_BYTES * 8)):
+ raise InvalidAmountError("amount does not fit in a 32-byte unsigned integer")
+ data = (
+ PROGRAM_INSTRUCTION_DISCRIMINATOR
+ + bytes([TRANSFER_REMOTE_VARIANT_TAG])
+ + destination_domain.to_bytes(4, "little")
+ + bytes(recipient32)
+ + amount.to_bytes(U256_BYTES, "little")
+ )
+ assert len(data) == INSTRUCTION_DATA_BYTES
+ return data
+
+
+# SEALEVEL_NOTES §4: AccountData> layout and compute_gas_fee constants.
+IGP_DISCRIMINATOR = b"IGP_____"
+TOKEN_EXCHANGE_RATE_SCALE = 10 ** 19 # exchange rate 1.0 is stored as 10^19
+SOL_DECIMALS = 9
+GAS_ORACLE_ENTRY_BYTES = 38 # [4B domain][1B tag][16B exchange rate][16B gas price][1B decimals]
+REMOTE_GAS_DATA_TAG = 0 # the only GasOracle variant defined today
+
+
+@dataclass(frozen=True)
+class GasOracle:
+ token_exchange_rate: int
+ gas_price: int
+ token_decimals: int
+
+
+@dataclass(frozen=True)
+class IgpAccount:
+ bump: int
+ salt: bytes
+ owner: str | None
+ beneficiary: str
+ gas_oracles: dict[int, GasOracle]
+ unsupported_oracles: dict[int, int] # domain -> variant tag, for entries that are not RemoteGasData
+
+
+class _Cursor:
+ """Little-endian Borsh reader over immutable bytes."""
+
+ def __init__(self, data: bytes) -> None:
+ self._data = bytes(data)
+ self._offset = 0
+
+ def take(self, size: int) -> bytes:
+ end = self._offset + size
+ if end > len(self._data):
+ raise ConfigurationError("malformed Sealevel IGP account data: declared layout exceeds the supplied bytes")
+ chunk = self._data[self._offset:end]
+ self._offset = end
+ return chunk
+
+ def u8(self) -> int:
+ return self.take(1)[0]
+
+ def u32(self) -> int:
+ return int.from_bytes(self.take(4), "little")
+
+ def u128(self) -> int:
+ return int.from_bytes(self.take(16), "little")
+
+ def pubkey(self) -> str:
+ return b58encode(self.take(32))
+
+
+def decode_igp_account(data: bytes) -> IgpAccount:
+ """Decode the terminal ``Igp`` account (the ``inner`` of an OverheadIgp), SEALEVEL_NOTES §4."""
+ cursor = _Cursor(data)
+ if cursor.u8() != 1:
+ raise ConfigurationError("Sealevel IGP account is not initialized")
+ discriminator = cursor.take(8)
+ if discriminator != IGP_DISCRIMINATOR:
+ raise ConfigurationError(
+ f"Sealevel IGP account has an unexpected discriminator {discriminator!r}; expected {IGP_DISCRIMINATOR!r}"
+ )
+ bump = cursor.u8()
+ salt = cursor.take(32)
+ owner_tag = cursor.u8()
+ if owner_tag not in (0, 1):
+ raise ConfigurationError(f"malformed Sealevel IGP account data: unsupported owner option tag {owner_tag}")
+ owner = cursor.pubkey() if owner_tag == 1 else None
+ beneficiary = cursor.pubkey()
+ count = cursor.u32()
+ oracles: dict[int, GasOracle] = {}
+ unsupported: dict[int, int] = {}
+ for _ in range(count):
+ domain = cursor.u32()
+ tag = cursor.u8()
+ exchange_rate = cursor.u128()
+ gas_price = cursor.u128()
+ decimals = cursor.u8()
+ if tag == REMOTE_GAS_DATA_TAG:
+ oracles[domain] = GasOracle(exchange_rate, gas_price, decimals)
+ else:
+ unsupported[domain] = tag
+ return IgpAccount(bump, salt, owner, beneficiary, oracles, unsupported)
+
+
+def igp_lamports(oracle: GasOracle, gas_amount: int) -> int:
+ """``compute_gas_fee`` + ``convert_decimals`` (SEALEVEL_NOTES §4), exact integer arithmetic."""
+ destination_cost = gas_amount * oracle.gas_price
+ origin_cost = destination_cost * oracle.token_exchange_rate // TOKEN_EXCHANGE_RATE_SCALE
+ if oracle.token_decimals <= SOL_DECIMALS:
+ return origin_cost * 10 ** (SOL_DECIMALS - oracle.token_decimals)
+ return origin_cost // 10 ** (oracle.token_decimals - SOL_DECIMALS)
+
+
+def quote_igp_lamports(igp_account_data: bytes, destination_domain: int, gas_amount: int) -> int:
+ """Lamports the IGP charges to deliver ``gas_amount`` destination gas to ``destination_domain``.
+
+ ``gas_amount`` is the warp token's ``destination_gas`` for the domain (route metadata
+ ``destinationGasAmount``, 464000 for Aleo), not derived from the message.
+ """
+ account = decode_igp_account(igp_account_data)
+ if destination_domain in account.unsupported_oracles:
+ tag = account.unsupported_oracles[destination_domain]
+ raise ConfigurationError(
+ f"Sealevel IGP account has an unexpected GasOracle variant tag {tag} for domain {destination_domain}; "
+ "only variant 0 (RemoteGasData) is decoded"
+ )
+ oracle = account.gas_oracles.get(destination_domain)
+ if oracle is None:
+ raise ConfigurationError(f"Sealevel IGP account has no gas-oracle entry for destination domain {destination_domain}")
+ return igp_lamports(oracle, gas_amount)
+
+
+# --- Route metadata ----------------------------------------------------------------------------
+
+SOLANA_ROUTE_ID = "hyperlane:solana/sol->aleo/sol"
+SYSTEM_PROGRAM_ADDRESS = "11111111111111111111111111111111"
+SOLANA_PUBKEY_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
+
+# SEALEVEL_NOTES §3: seeds are separate byte strings (separators are their own seed).
+DISPATCHED_MESSAGE_SEED_PREFIX = (b"hyperlane", b"-", b"dispatched_message", b"-")
+GAS_PAYMENT_SEED_PREFIX = (b"hyperlane_igp", b"-", b"gas_payment", b"-")
+PDA_MARKER = b"ProgramDerivedAddress"
+MAX_SEEDS = 16
+MAX_SEED_LENGTH = 32
+
+# veil protocols/hyperlane/solana.ts: rent for the two accounts a transfer creates (gas-payment PDA,
+# dispatched-message PDA) plus the sender's own rent floor; compute-unit limit set on every transfer.
+GAS_PAYMENT_ACCOUNT_DATA_LENGTH = 141
+DISPATCHED_MESSAGE_ACCOUNT_DATA_LENGTH = 194
+COMPUTE_UNIT_LIMIT = 400_000
+
+
+@dataclass(frozen=True)
+class SolanaRouteMetadata:
+ warp_program_address: str
+ token_pda: str
+ native_collateral_pda: str
+ dispatch_authority_pda: str
+ mailbox_program_address: str
+ mailbox_outbox_pda: str
+ igp_program_address: str
+ igp_program_data_pda: str
+ igp_account: str
+ igp_overhead_account: str | None
+ spl_noop_program_address: str
+ destination_domain: int
+ destination_gas_amount: int
+ registry_commit: str
+ solana_reviewed_at: str
+ solana_config_source: str
+
+
+_PUBKEY_FIELDS = (
+ ("warpProgramAddress", "warp_program_address"),
+ ("tokenPda", "token_pda"),
+ ("nativeCollateralPda", "native_collateral_pda"),
+ ("dispatchAuthorityPda", "dispatch_authority_pda"),
+ ("mailboxProgramAddress", "mailbox_program_address"),
+ ("mailboxOutboxPda", "mailbox_outbox_pda"),
+ ("igpProgramAddress", "igp_program_address"),
+ ("igpProgramDataPda", "igp_program_data_pda"),
+ ("igpAccount", "igp_account"),
+ ("splNoopProgramAddress", "spl_noop_program_address"),
+)
+
+
+def solana_route_metadata(route: Route) -> SolanaRouteMetadata:
+ """Validate and return the reviewed Solana deployment metadata (veil ``solanaMetadata.ts``).
+
+ Every address participates in instruction account ordering, so the whole route is
+ refused when one field is missing or malformed rather than letting a bad key through.
+ """
+ if route.protocol != "hyperlane":
+ raise RouteUnavailableError(f"Solana Hyperlane actions require a Hyperlane route, got {route.protocol}: {route.id}")
+ if route.availability != "active":
+ raise RouteUnavailableError(f"Hyperlane route is not executable: {route.id} ({route.availability})")
+ metadata: Mapping[str, object] = route.metadata or {}
+
+ def pubkey(key: str) -> str:
+ value = metadata.get(key)
+ if not isinstance(value, str) or not SOLANA_PUBKEY_RE.match(value):
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid {key}: {route.id}")
+ return value
+
+ fields = {attr: pubkey(key) for key, attr in _PUBKEY_FIELDS}
+ overhead = metadata.get("igpOverheadAccount")
+ fields["igp_overhead_account"] = None if overhead is None else pubkey("igpOverheadAccount")
+
+ domain = metadata.get("destinationDomain")
+ if isinstance(domain, bool) or not isinstance(domain, int) or not 0 <= domain <= 0xFFFF_FFFF:
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid destinationDomain: {route.id}")
+ gas = metadata.get("destinationGasAmount")
+ if isinstance(gas, bool) or not (isinstance(gas, int) or (isinstance(gas, str) and gas.isdigit())):
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid destinationGasAmount: {route.id}")
+ commit = metadata.get("registryCommit")
+ if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40}", commit):
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid registryCommit: {route.id}")
+ reviewed = metadata.get("solanaReviewedAt")
+ if not isinstance(reviewed, str) or not re.match(r"^\d{4}-\d{2}-\d{2}", reviewed):
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid solanaReviewedAt: {route.id}")
+ source = metadata.get("solanaConfigSource")
+ if not isinstance(source, str) or not source:
+ raise RouteUnavailableError(f"Solana Hyperlane route has an invalid solanaConfigSource: {route.id}")
+ return SolanaRouteMetadata(
+ destination_domain=domain,
+ destination_gas_amount=int(gas),
+ registry_commit=commit,
+ solana_reviewed_at=reviewed,
+ solana_config_source=source,
+ **fields,
+ )
+
+
+# --- PDAs -------------------------------------------------------------------------------------
+
+_ED25519_P = 2 ** 255 - 19
+_ED25519_D = (-121665 * pow(121666, -1, _ED25519_P)) % _ED25519_P
+
+
+def _is_on_curve(point: bytes) -> bool:
+ """Whether a compressed Edwards Y coordinate decompresses (curve25519-dalek ``decompress``):
+ x² = (y² − 1) / (d·y² + 1) must have a square root (or be zero)."""
+ y = (int.from_bytes(point, "little") & ((1 << 255) - 1)) % _ED25519_P
+ y2 = y * y % _ED25519_P
+ u = (y2 - 1) % _ED25519_P
+ v = (_ED25519_D * y2 + 1) % _ED25519_P
+ if v == 0:
+ return u == 0
+ x2 = u * pow(v, -1, _ED25519_P) % _ED25519_P
+ return x2 == 0 or pow(x2, (_ED25519_P - 1) // 2, _ED25519_P) == 1
+
+
+def create_program_address(seeds: Sequence[bytes], program_id: str) -> str | None:
+ """``Pubkey::create_program_address``: sha256(seeds ‖ program_id ‖ marker); None when on-curve."""
+ if len(seeds) > MAX_SEEDS:
+ raise BridgeError(f"program address derivation accepts at most {MAX_SEEDS} seeds")
+ digest = hashlib.sha256()
+ for seed in seeds:
+ if len(seed) > MAX_SEED_LENGTH:
+ raise BridgeError(f"each program address seed must be at most {MAX_SEED_LENGTH} bytes")
+ digest.update(bytes(seed))
+ digest.update(b58decode(program_id))
+ digest.update(PDA_MARKER)
+ candidate = digest.digest()
+ return None if _is_on_curve(candidate) else b58encode(candidate)
+
+
+def find_program_address(seeds: Sequence[bytes], program_id: str) -> tuple[str, int]:
+ """``Pubkey::find_program_address``: try bump seeds 255 → 0, return the first off-curve address."""
+ for bump in range(255, -1, -1):
+ address = create_program_address([*seeds, bytes([bump])], program_id)
+ if address is not None:
+ return address, bump
+ raise BridgeError("unable to find a viable program address bump seed")
+
+
+def derive_dispatched_message_pda(mailbox_program_address: str, unique_message_address: str) -> str:
+ """Mailbox ``["hyperlane","-","dispatched_message","-", unique_message_pubkey]`` (SEALEVEL_NOTES §3)."""
+ return find_program_address([*DISPATCHED_MESSAGE_SEED_PREFIX, b58decode(unique_message_address)], mailbox_program_address)[0]
+
+
+def derive_gas_payment_pda(igp_program_address: str, unique_message_address: str) -> str:
+ """IGP ``["hyperlane_igp","-","gas_payment","-", unique_message_pubkey]`` — same unique key as the message PDA."""
+ return find_program_address([*GAS_PAYMENT_SEED_PREFIX, b58decode(unique_message_address)], igp_program_address)[0]
+
+
+# --- Account table ----------------------------------------------------------------------------
+
+@dataclass(frozen=True)
+class SolanaAccountMeta:
+ address: str
+ signer: bool
+ writable: bool
+
+ def to_dict(self) -> dict[str, str | bool]:
+ return {"address": self.address, "signer": self.signer, "writable": self.writable}
+
+
+def account_metas(metadata: SolanaRouteMetadata, sender: str, unique_message: str) -> list[SolanaAccountMeta]:
+ """The native-collateral ``TransferRemote`` account list, SEALEVEL_NOTES §2 rows 0–15.
+
+ Row 12 (``igpOverheadAccount``) is present only when the route wraps its IGP in an
+ OverheadIgp; the list then has 16 entries, otherwise 15. The sender compiles writable
+ (the native-collateral ``transfer_in`` CPI debits it) and the unique-message account is
+ a read-only signer.
+ """
+ def ro(address: str) -> SolanaAccountMeta:
+ return SolanaAccountMeta(address, False, False)
+
+ def rw(address: str) -> SolanaAccountMeta:
+ return SolanaAccountMeta(address, False, True)
+
+ dispatched_message = derive_dispatched_message_pda(metadata.mailbox_program_address, unique_message)
+ gas_payment = derive_gas_payment_pda(metadata.igp_program_address, unique_message)
+ metas = [
+ ro(SYSTEM_PROGRAM_ADDRESS), # 0
+ ro(metadata.spl_noop_program_address), # 1
+ ro(metadata.token_pda), # 2
+ ro(metadata.mailbox_program_address), # 3
+ rw(metadata.mailbox_outbox_pda), # 4
+ ro(metadata.dispatch_authority_pda), # 5
+ SolanaAccountMeta(sender, True, True), # 6 sender / fee payer
+ SolanaAccountMeta(unique_message, True, False), # 7 unique message (readonly signer)
+ rw(dispatched_message), # 8
+ ro(metadata.igp_program_address), # 9
+ rw(metadata.igp_program_data_pda), # 10
+ rw(gas_payment), # 11
+ ]
+ if metadata.igp_overhead_account is not None:
+ metas.append(ro(metadata.igp_overhead_account)) # 12 (optional)
+ metas.extend([
+ rw(metadata.igp_account), # 13
+ ro(SYSTEM_PROGRAM_ADDRESS), # 14
+ rw(metadata.native_collateral_pda), # 15
+ ])
+ return metas
+
+
+# --- Program logs ------------------------------------------------------------------------------
+
+# SEALEVEL_NOTES §5: only the Mailbox dispatch line carries the full id; the IGP and warp-completion
+# lines print H256 with Display (truncated "0xffe0…7805") and must never be parsed.
+# The trailing lookahead refuses a 65+-hex id outright rather than truncating it to a plausible-looking one.
+DISPATCHED_MESSAGE_LOG_PATTERN = re.compile(r"Dispatched message to \d+, ID (0x[0-9a-fA-F]{64})(?![0-9a-fA-F])")
+
+
+def extract_hyperlane_message_id(logs: "list[str] | None") -> str | None:
+ """The 32-byte Hyperlane message id from confirmed program logs, or None when absent."""
+ if not logs:
+ return None
+ for line in logs:
+ match = DISPATCHED_MESSAGE_LOG_PATTERN.search(line)
+ if match:
+ return match.group(1)
+ return None
diff --git a/bridge-sdk/python/aleo_bridge/agent.py b/bridge-sdk/python/aleo_bridge/agent.py
new file mode 100644
index 0000000..d647e9b
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/agent.py
@@ -0,0 +1,483 @@
+"""Framework-neutral agent tools over a :class:`~aleo_bridge.client.Bridge`.
+
+``bridge_tools()`` returns tool definitions in the Claude API ``tools=`` shape
+(name / description / input_schema); ``dispatch_tool(bridge, name, args)``
+executes one and returns JSON-serializable data. Reads are open. Writes
+require ``confirm: true`` — without it they return the quote (or recovered
+progress / built call) plus ``how_to_confirm`` and move nothing. Agents never
+carry ``Plan`` objects: ``bridge_execute`` takes the quote inputs and
+re-quotes internally; recovery tools take the checkpoint dict returned by the
+previous write. Amounts in and out are human units; ints stay ints.
+
+Three rules keep a model from doing damage with this surface:
+
+* **Secrets never leave the process.** ``_serialize`` drops the private-mint
+ secret, the Circle attestation body and the proved transaction bytes from any
+ receipt it renders, a previewed program call renders record-shaped inputs as
+ ``""``, and no tool ever echoes its own arguments back. (The
+ xReserve *hook data* — a public commitment, not the secret that opens it —
+ stays on the quote and inside the checkpoint, because ``lifecycle.resume``
+ refuses to resume a deposit whose checkpoint has lost it.)
+* **A private mint never gets a default secret.** ``mint_mode="private"``
+ without a ``secret_nonce`` is a structured error, never a quiet ``"0scalar"``
+ that commits to a hook nobody can reproduce.
+* **Failures come back as data.** Every :class:`~aleo_bridge.errors.BridgeError`
+ is rendered ``{"error", "error_type", "how_to_fix"}`` so the model can fix the
+ input or set up the missing connection instead of crashing its own loop; a
+ send whose outcome is ambiguous also carries ``{"next": "recover"}`` and the
+ last checkpoint. Programming errors (``ValueError``, ``TypeError``) still
+ propagate.
+"""
+from __future__ import annotations
+
+import copy
+import dataclasses
+import enum
+import re
+from typing import Any, Callable
+
+from . import lifecycle
+from .checkpoint import Checkpoint, create_checkpoint
+from .errors import BridgeError, ConfigurationError
+from .registry import DEFAULT_REGISTRY, Registry
+from .types import EvmXReserveQuote, Receipt
+from .units import format_decimal_amount
+
+_S = {"type": "string"}
+_I = {"type": "integer"}
+_B = {"type": "boolean"}
+HOW_TO_CONFIRM = "re-call with confirm=true"
+
+EVM_HOW_TO_FIX = ("set EVM_PRIVATE_KEY + ETHEREUM_RPC_URL in the environment (or pass "
+ "ethereum=Ethereum(...) to Bridge(...)) and retry")
+SOLANA_HOW_TO_FIX = ("set SOLANA_PRIVATE_KEY (and optionally SOLANA_RPC_URL) in the environment (or pass "
+ "solana=Solana(...) to Bridge(...)) and retry")
+NONCE_HOW_TO_FIX = ("re-call with secret_nonce set to the value the user kept from execute — the SDK "
+ "never stored it, and no other value can open the commitment")
+RECOVER_HOW_TO_FIX = ("the source step may already be on the wire: call bridge_get_progress with the "
+ "checkpoint (or bridge_pending) before doing anything else — never bridge_execute again")
+
+#: Receipt ``protocol_state`` keys that are dropped from every rendered receipt: the private-mint
+#: secret and record plaintext (secret), the Circle attestation body (secret-adjacent and large),
+#: the proved transaction bytes and the hook commitment (both live in the checkpoint, which is what
+#: the recovery verbs actually consume).
+_REDACTED_STATE_KEYS = frozenset({
+ "payload", "attestation", "secretnonce", "record", "recordplaintext", "privatekey",
+ "hookdata", "preparedtransaction", "prepareddestinationtransaction",
+})
+
+
+def _schema(properties: dict[str, Any], required: list[str]) -> dict[str, Any]:
+ return {"type": "object", "properties": properties, "required": required}
+
+
+# ── serialization ─────────────────────────────────────────────────────────────
+
+def _max_fee_entry(quote: EvmXReserveQuote, registry: Registry | None) -> dict[str, Any] | None:
+ """The xReserve max fee as a fee entry: ``EvmXReserveQuote.fees`` is empty, and an agent that
+ cannot see the fee cannot tell the user what the transfer costs."""
+ plan = quote.plan
+ if plan is None:
+ return None
+ try:
+ asset = (registry or DEFAULT_REGISTRY).asset(plan.source_asset_id)
+ amount = format_decimal_amount(quote.max_fee_atomic, asset.decimals)
+ except BridgeError:
+ return None
+ return {"kind": "protocol", "chain_id": asset.chain_id, "asset_id": asset.id, "amount": amount,
+ "estimated": True, "label": "xReserve max fee"}
+
+
+def _serialize(value: Any, registry: Registry | None = None) -> Any:
+ """Dataclasses → dicts, bytes → 0x hex, enums → values, tuples → lists; ints stay ints.
+
+ Checkpoints render through ``to_dict()`` (the camelCase form the recovery tools take back),
+ receipts lose their secret / bulky ``protocol_state`` entries, and an ``EvmXReserveQuote``
+ gains the synthetic max-fee entry its empty ``fees`` tuple would otherwise hide.
+ """
+ if isinstance(value, Checkpoint):
+ return value.to_dict()
+ if isinstance(value, Receipt):
+ return {f.name: (_redacted_state(getattr(value, f.name)) if f.name == "protocol_state"
+ else _serialize(getattr(value, f.name), registry))
+ for f in dataclasses.fields(value)}
+ if isinstance(value, EvmXReserveQuote):
+ out = {f.name: _serialize(getattr(value, f.name), registry) for f in dataclasses.fields(value)}
+ entry = _max_fee_entry(value, registry)
+ if entry is not None:
+ out["fees"] = list(out.get("fees") or []) + [entry]
+ return out
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
+ return {f.name: _serialize(getattr(value, f.name), registry) for f in dataclasses.fields(value)}
+ if isinstance(value, enum.Enum):
+ return value.value
+ if isinstance(value, (bytes, bytearray)):
+ return "0x" + bytes(value).hex()
+ if isinstance(value, dict):
+ return {str(k): _serialize(v, registry) for k, v in value.items()}
+ if isinstance(value, (list, tuple, set)):
+ return [_serialize(v, registry) for v in value]
+ if isinstance(value, (str, int, float, bool)) or value is None:
+ return value
+ return str(value)
+
+
+#: A record, in any form an Aleo call input can carry it: the plaintext ``{ owner: … }`` a record
+#: selection returns (``privacy.py``'s ``select_record``), the ``record1…`` ciphertext, a
+#: ``x.record`` locator, or any plaintext carrying a nonce / a credits amount. A record IS the
+#: private balance — showing one to a model (or writing it into a transcript) spends its privacy.
+_RECORD_SHAPED = re.compile(r"""
+ (^record1[a-z0-9]{8,}) # record ciphertext
+ | (\.record\b) # a record locator
+ | (^\{\s*owner\s*:) # record plaintext, as select_record returns it
+ | (\b_nonce\s*:) # …or anything else carrying a record's nonce
+ | (\bmicrocredits\s*:) # …or a credits record's amount
+""", re.IGNORECASE | re.VERBOSE)
+
+
+def _summarize_input(value: Any) -> str:
+ """One program-call input, rendered for a human or a model: record-shaped → ``""``.
+
+ Everything public — the amount literal, the recipient address, a Merkle path — passes through,
+ so the preview still says what the call does.
+ """
+ text = value if isinstance(value, str) else str(value)
+ return "" if _RECORD_SHAPED.search(text.strip()) else text
+
+
+def _redacted_state(state: Any) -> Any:
+ if not isinstance(state, dict):
+ return _serialize(state)
+ return {str(k): _serialize(v) for k, v in state.items()
+ if str(k).replace("_", "").lower() not in _REDACTED_STATE_KEYS}
+
+
+# ── errors ────────────────────────────────────────────────────────────────────
+
+def _how_to_fix(exc: Exception) -> str | None:
+ message = str(exc).lower()
+ if "secret_nonce" in message or "secret nonce" in message:
+ return NONCE_HOW_TO_FIX
+ if "evm_private_key" in message or "ethereum_rpc_url" in message or "ethereum=ethereum" in message:
+ return EVM_HOW_TO_FIX
+ if "solana" in message and ("not configured" in message or "solana_private_key" in message):
+ return SOLANA_HOW_TO_FIX
+ return None
+
+
+def _error_payload(exc: Exception, **extra: Any) -> dict[str, Any]:
+ payload: dict[str, Any] = {"error": str(exc), "error_type": exc.__class__.__name__}
+ fix = _how_to_fix(exc)
+ if fix is not None:
+ payload["how_to_fix"] = fix
+ payload.update(extra)
+ return payload
+
+
+def _connection_gap(bridge: Any, source: Any) -> dict[str, Any] | None:
+ """Probe the connection a transfer out of *source* would sign with (spec §10: a missing
+ connection is a configuration answer the model can act on, not an exception)."""
+ try:
+ asset = bridge.registry.asset(source)
+ family = bridge.registry.chain(asset.chain_id).family
+ except BridgeError:
+ return None # let the real lookup produce the real error
+ if family == "evm" and getattr(bridge, "ethereum", None) is None:
+ return _error_payload(ConfigurationError(
+ f"No Ethereum connection is configured, and {asset.id} transfers are signed on "
+ f"{asset.chain_id}."), how_to_fix=EVM_HOW_TO_FIX)
+ if family == "solana" and getattr(bridge, "solana", None) is None:
+ return _error_payload(ConfigurationError(
+ f"No Solana connection is configured, and {asset.id} transfers are signed on "
+ f"{asset.chain_id}."), how_to_fix=SOLANA_HOW_TO_FIX)
+ return None
+
+
+def _missing_nonce(what: str) -> dict[str, Any]:
+ return _error_payload(ConfigurationError(
+ f"a secret_nonce is required for a private mint: {what} commits to (recipient, secret_nonce) "
+ "and bridge_complete needs the same value again — keep it, the SDK never stores it"),
+ how_to_fix=NONCE_HOW_TO_FIX)
+
+
+# ── shared argument handling ──────────────────────────────────────────────────
+
+_QUOTE_PROPS = {
+ "source": {**_S, "description": "Source asset as 'chain/key', e.g. 'ethereum/usdc', 'aleo/eth', 'solana/sol'."},
+ "destination": {**_S, "description": "Destination asset as 'chain/key', e.g. 'aleo/usdcx', 'ethereum/eth'."},
+ "amount": {**_S, "description": "Positive decimal amount in source-asset display units (e.g. '2', '0.001')."},
+ "recipient": {**_S, "description": "Destination-chain address that receives the funds."},
+ "sender": {**_S, "description": "Optional source-chain address; must be the configured connection's address."},
+ "protocol": {**_S, "enum": ["xreserve", "hyperlane"], "description": "Only needed when both protocols serve the pair."},
+ "mint_mode": {**_S, "enum": ["public", "record", "private"],
+ "description": "xReserve into Aleo only. 'private' requires the user to complete the mint later with the same secret_nonce."},
+ "secret_nonce": {**_S, "description": "Private-mint commitment secret, REQUIRED when mint_mode is 'private' "
+ "(there is no default). The user must keep it for bridge_complete; the SDK never stores it."},
+}
+_QUOTE_REQUIRED = ["source", "destination", "amount", "recipient"]
+_CONFIRM = {"confirm": {**_B, "description": "Set true to move funds. Without it the quote is returned and nothing is submitted."}}
+_CHECKPOINT = {"checkpoint": {"type": "object",
+ "description": "The checkpoint dict returned by bridge_get_progress, by an entry of "
+ "bridge_pending, or by bridge_execute / bridge_resume / bridge_complete "
+ "(including the one an interrupted write hands back with next='recover')."}}
+
+
+def _quote_kwargs(args: dict[str, Any]) -> dict[str, Any]:
+ """Quote inputs. ``secret_nonce`` defaults to ``"0scalar"`` only for a non-private mint —
+ a private one must carry its own (checked by :func:`_private_nonce_gap` first)."""
+ mint_mode = args.get("mint_mode") or "public"
+ secret_nonce = args.get("secret_nonce")
+ return dict(source=args["source"], destination=args["destination"], amount=str(args["amount"]),
+ recipient=args["recipient"], sender=args.get("sender"), protocol=args.get("protocol"),
+ mint_mode=mint_mode,
+ secret_nonce=secret_nonce if mint_mode == "private" else (secret_nonce or "0scalar"))
+
+
+def _private_nonce_gap(args: dict[str, Any]) -> dict[str, Any] | None:
+ if (args.get("mint_mode") or "public") == "private" and not args.get("secret_nonce"):
+ return _missing_nonce("the deposit")
+ return None
+
+
+def _with_checkpoint(bridge: Any, progress: Any) -> dict[str, Any]:
+ checkpoint = create_checkpoint(progress.plan, progress.receipt, bridge.registry)
+ return {"progress": _serialize(progress, bridge.registry), "checkpoint": checkpoint.to_dict()}
+
+
+def _confirmation(**payload: Any) -> dict[str, Any]:
+ return {"confirmation_required": True, **payload, "how_to_confirm": HOW_TO_CONFIRM}
+
+
+def _write(b: Any, call: Callable[[list[Any]], Any]) -> dict[str, Any]:
+ """Run one fund-moving verb, collecting its checkpoints, and render either outcome.
+
+ Success is ``{"progress", "checkpoint"}``. A failure is never a retry cue: every write here is
+ single-use and a lost RPC answer is ambiguous, so the error comes back with ``next: "recover"``,
+ how-to-fix pointing at ``bridge_get_progress``, and the last checkpoint that made it out —
+ which, for a write interrupted after proving, is the only copy of those bytes.
+ """
+ seen: list[Any] = []
+ try:
+ progress = call(seen)
+ except BridgeError as exc:
+ payload = _error_payload(exc, next="recover")
+ payload["how_to_fix"] = RECOVER_HOW_TO_FIX
+ if seen:
+ payload["checkpoint"] = _serialize(seen[-1], b.registry)
+ return payload
+ return _with_checkpoint(b, progress)
+
+
+# ── reads ─────────────────────────────────────────────────────────────────────
+
+def _h_status(b, a):
+ return _serialize(b.status(), b.registry)
+
+
+def _h_list_assets(b, a):
+ return _serialize(b.registry.assets(chain=a.get("chain"), symbol=a.get("symbol"),
+ environment=a.get("environment", b.environment)), b.registry)
+
+
+def _h_list_routes(b, a):
+ return _serialize(b.registry.routes(source=a.get("source"), destination=a.get("destination"),
+ protocol=a.get("protocol"), symbol=a.get("symbol"),
+ include_unavailable=bool(a.get("include_unavailable", False)),
+ environment=a.get("environment", b.environment)), b.registry)
+
+
+def _h_quote(b, a):
+ gap = _private_nonce_gap(a) or _connection_gap(b, a.get("source"))
+ if gap is not None:
+ return gap
+ return _serialize(lifecycle.quote(b, **_quote_kwargs(a)), b.registry)
+
+
+def _h_get_progress(b, a):
+ # A checkpoint comes back with the progress: an agent that started from a stale one (or from
+ # bridge_pending) can hand this one straight to bridge_resume / bridge_complete.
+ return _with_checkpoint(b, lifecycle.recover(b, a["checkpoint"]))
+
+
+def _h_pending(b, a):
+ """Every stored checkpoint with the ``Progress`` reconstructed for it — offline, one entry each.
+
+ Reconstruction is :func:`lifecycle.progress_from_checkpoint`, exactly what ``Bridge.pending()``
+ runs per record (no network read, so one unreachable chain can never hide the others). It is
+ called here rather than through ``Bridge.pending()`` because that verb returns bare ``Progress``
+ objects: it neither pairs each one with the checkpoint that produced it — which is what the
+ recovery tools take back, and must be the STORED record, not one re-derived from a receipt an
+ offline reconstruction may have flattened — nor reports the records it could not interpret at
+ all (it drops them). Here such a record becomes one error entry naming its checkpoint id, and
+ the healthy entries still come back.
+ """
+ store = getattr(b, "checkpoints", None)
+ if store is None:
+ return []
+ out: list[dict[str, Any]] = []
+ for cp in store.list():
+ try:
+ progress = lifecycle.progress_from_checkpoint(b.registry, cp)
+ except BridgeError as exc:
+ out.append(_error_payload(exc, checkpoint_id=cp.id))
+ continue
+ out.append({"progress": _serialize(progress, b.registry), "checkpoint": cp.to_dict()})
+ return out
+
+
+# ── writes (confirm-gated) ────────────────────────────────────────────────────
+
+def _h_execute(b, a):
+ gap = _private_nonce_gap(a) or _connection_gap(b, a.get("source"))
+ if gap is not None:
+ return gap
+ quote = lifecycle.quote(b, **_quote_kwargs(a))
+ if not a.get("confirm"):
+ return _confirmation(quote=_serialize(quote, b.registry))
+ return _write(b, lambda seen: lifecycle.execute(
+ b, quote.plan, on_checkpoint=seen.append, mode=a.get("mode"), proving=a.get("proving", "delegate"),
+ gas_payment_microcredits=a.get("gas_payment_microcredits"), secret_nonce=a.get("secret_nonce")))
+
+
+def _h_resume(b, a):
+ progress = lifecycle.recover(b, a["checkpoint"]) # reads only
+ if not a.get("confirm"):
+ return _confirmation(progress=_serialize(progress, b.registry))
+ return _write(b, lambda seen: lifecycle.resume(b, progress, on_checkpoint=seen.append,
+ secret_nonce=a.get("secret_nonce")))
+
+
+def _has_prepared_destination(progress: Any) -> bool:
+ """A mint whose bytes are already proved rebroadcasts without the secret."""
+ state = getattr(progress.receipt, "protocol_state", {}) or {}
+ return bool(state.get("preparedDestinationTransaction"))
+
+
+def _h_complete(b, a):
+ progress = lifecycle.recover(b, a["checkpoint"]) # reads only
+ secret_nonce = a.get("secret_nonce")
+ if not secret_nonce and not _has_prepared_destination(progress):
+ return _missing_nonce("the deposit this mint finishes")
+ if not a.get("confirm"):
+ return _confirmation(progress=_serialize(progress, b.registry))
+ return _write(b, lambda seen: lifecycle.complete(b, progress, on_checkpoint=seen.append,
+ secret_nonce=secret_nonce))
+
+
+def _privacy(b, a, direction: str):
+ kwargs = dict(asset=a["asset"], amount=a.get("amount"), amount_atomic=a.get("amount_atomic"))
+ call = b.shield(**kwargs) if direction == "shield" else b.unshield(**kwargs)
+ if not a.get("confirm"):
+ # An unshield's inputs carry the selected record's plaintext — the private balance itself.
+ return _confirmation(call={"program": call.program_id, "function": call.function_name,
+ "inputs": [_summarize_input(i) for i in call.inputs]})
+ return _serialize(call.delegate(), b.registry)
+
+
+def _h_shield(b, a):
+ return _privacy(b, a, "shield")
+
+
+def _h_unshield(b, a):
+ return _privacy(b, a, "unshield")
+
+
+# ── tool table ────────────────────────────────────────────────────────────────
+
+_READ_TOOLS: list[tuple[str, str, dict[str, Any], Callable[[Any, dict[str, Any]], Any]]] = [
+ ("bridge_status",
+ "Re-orient: environment, registry version, the configured Aleo/Ethereum/Solana addresses with balances of "
+ "every bridge asset (atomic units), and pending (in-flight) transfers. Run this FIRST in any session.",
+ _schema({}, []), _h_status),
+ ("bridge_list_assets",
+ "Assets that can be bridged, with chain, symbol, decimals and on-chain locator. Filter by chain id "
+ "(aleo, ethereum, solana, aleo-testnet, sepolia) or symbol.",
+ _schema({"chain": _S, "symbol": _S, "environment": {**_S, "enum": ["mainnet", "testnet"]}}, []), _h_list_assets),
+ ("bridge_list_routes",
+ "Supported directions and their protocol (xreserve = USDC<->USDCx via Circle; hyperlane = ETH/WBTC/USDT/SOL). "
+ "Active routes move funds; metadata-required ones are listed but refused by quote/execute.",
+ _schema({"source": _S, "destination": _S, "protocol": {**_S, "enum": ["xreserve", "hyperlane"]}, "symbol": _S,
+ "include_unavailable": _B, "environment": {**_S, "enum": ["mainnet", "testnet"]}}, []), _h_list_routes),
+ ("bridge_quote",
+ "Validate and price a transfer: route, fees (human units), amount_out, approval needs. Reads chain state, "
+ "never signs. ALWAYS quote before bridge_execute and show the user fees and amount_out. A private mint "
+ "(mint_mode='private') must carry the user's own secret_nonce — there is no default.",
+ _schema(_QUOTE_PROPS, _QUOTE_REQUIRED), _h_quote),
+ ("bridge_get_progress",
+ "Recover a transfer's state from a checkpoint (reads only). Returns {progress, checkpoint}: progress.next tells "
+ "what to do — wait (call again later), resume (bridge_resume), complete (bridge_complete), done, failed — and "
+ "the checkpoint is the fresh one to pass to whichever of those you call.",
+ _schema(_CHECKPOINT, ["checkpoint"]), _h_get_progress),
+ ("bridge_pending",
+ "Every in-flight transfer in this profile's checkpoint store, one {progress, checkpoint} entry each, "
+ "reconstructed offline (no chain read, so its progress can lag: bridge_get_progress refreshes one against live "
+ "state). A record too damaged to interpret comes back as {error, checkpoint_id} in its place.",
+ _schema({}, []), _h_pending),
+]
+
+_WRITE_TOOLS: list[tuple[str, str, dict[str, Any], Callable[[Any, dict[str, Any]], Any]]] = [
+ ("bridge_execute",
+ "Start a transfer: re-quotes the same inputs, then commits funds on the source chain. Requires confirm=true; "
+ "without it returns the quote and moves nothing. The source step is IRREVERSIBLE once broadcast — afterwards "
+ "use bridge_get_progress with the returned checkpoint, never bridge_execute again. For mint_mode=private the "
+ "user must supply secret_nonce here (no default) and keep it for bridge_complete.",
+ _schema({**_QUOTE_PROPS, "mode": {**_S, "description": "Aleo-origin only: caller|signer (Hyperlane) or "
+ "private|public|public-as-signer (xReserve burn)."},
+ "gas_payment_microcredits": _I, "proving": {**_S, "enum": ["delegate", "local"]}, **_CONFIRM},
+ _QUOTE_REQUIRED), _h_execute),
+ ("bridge_resume",
+ "Finish an interrupted source submission (progress.next == 'resume'): rebroadcasts the identical proved Aleo "
+ "transaction or authorizes the single missing EVM step after re-scanning history. Requires confirm=true. "
+ "An interrupted EVM xReserve deposit needs the same secret_nonce used at execute.",
+ _schema({**_CHECKPOINT, "secret_nonce": {**_S, "description": "The value used at bridge_execute; required to "
+ "resume a private-mint xReserve deposit."},
+ **_CONFIRM}, ["checkpoint"]), _h_resume),
+ ("bridge_complete",
+ "Submit the private USDCx mint (progress.next == 'complete') with the secret_nonce used at execute. "
+ "Requires confirm=true. Submits exactly one Aleo transaction.",
+ _schema({**_CHECKPOINT,
+ "secret_nonce": {**_S, "description": "The private-mint secret used at bridge_execute — required "
+ "(there is no default); only an already-proved mint can be "
+ "rebroadcast without it."},
+ **_CONFIRM}, ["checkpoint"]), _h_complete),
+ ("bridge_shield",
+ "Move a public Aleo balance of a bridged asset (aleo/eth, aleo/wbtc, aleo/usdt, aleo/sol, aleo/usdcx) into a "
+ "private record. Requires confirm=true; without it returns the program call for review.",
+ _schema({"asset": _S, "amount": _S, "amount_atomic": _I, **_CONFIRM}, ["asset"]), _h_shield),
+ ("bridge_unshield",
+ "Move a private record of a bridged asset back to the public balance (needed before an Aleo-origin Hyperlane "
+ "transfer). Requires confirm=true.",
+ _schema({"asset": _S, "amount": _S, "amount_atomic": _I, **_CONFIRM}, ["asset"]), _h_unshield),
+]
+
+_HANDLERS: dict[str, Callable[[Any, dict[str, Any]], Any]] = {
+ name: handler for name, _, _, handler in _READ_TOOLS + _WRITE_TOOLS}
+
+
+def bridge_tools(include_writes: bool = True) -> list[dict[str, Any]]:
+ """Tool definitions (Claude API ``tools=`` shape); ``include_writes=False`` keeps only reads.
+
+ Every call returns a fresh deep copy: a caller that tailors a schema (or a framework that
+ annotates one in place) can never edit the module's own table out from under everyone else.
+ """
+ tools = _READ_TOOLS + (_WRITE_TOOLS if include_writes else [])
+ return [{"name": name, "description": desc, "input_schema": copy.deepcopy(schema)}
+ for name, desc, schema, _ in tools]
+
+
+def dispatch_tool(bridge: Any, name: str, args: dict[str, Any] | None = None) -> Any:
+ """Execute one tool against *bridge*; returns JSON-serializable data.
+
+ A :class:`~aleo_bridge.errors.BridgeError` becomes ``{"error", "error_type", ...}`` — the model
+ gets to fix the input rather than lose its loop. An unknown tool name is a ``ValueError``.
+ """
+ handler = _HANDLERS.get(name)
+ if handler is None:
+ raise ValueError(f"Unknown bridge tool: {name!r}")
+ try:
+ return handler(bridge, dict(args or {}))
+ except BridgeError as exc:
+ return _error_payload(exc)
+
+
+__all__ = ["bridge_tools", "dispatch_tool", "HOW_TO_CONFIRM"]
diff --git a/bridge-sdk/python/aleo_bridge/checkpoint.py b/bridge-sdk/python/aleo_bridge/checkpoint.py
new file mode 100644
index 0000000..6b42d79
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/checkpoint.py
@@ -0,0 +1,256 @@
+"""Checkpoints — the allowlisted, versioned recovery record (spec §8, brief §2.10).
+
+A checkpoint carries the public transfer intent, the route id + registry
+version, and the transaction ids already submitted (plus, for Aleo legs, the
+exact proved-but-unbroadcast transaction). It never carries keys, record
+plaintext, the private-mint secret nonce, attestation bodies, payloads,
+message hashes, nonces, or quote internals — ``create_checkpoint`` copies an
+allowlist, not ``protocol_state``.
+"""
+from __future__ import annotations
+
+import json
+import os
+import re
+import tempfile
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Protocol, runtime_checkable
+
+from .errors import CheckpointInvalidError
+from .registry import Registry
+from .types import Plan, Receipt
+
+CHECKPOINT_VERSION = 1
+_DIGITS = re.compile(r"^\d+$")
+
+
+@dataclass(frozen=True)
+class Checkpoint:
+ """Version-1 recovery record. Dict/JSON keys are veil's camelCase names."""
+
+ version: int
+ receipt_id: str
+ intent: dict[str, Any]
+ route: dict[str, str]
+ source: dict[str, Any] | None = None
+ destination: dict[str, Any] | None = None
+ delivery_verification: dict[str, str] | None = None
+
+ @property
+ def id(self) -> str:
+ """The receipt id this checkpoint was created from (store key)."""
+ return self.receipt_id
+
+ def to_dict(self) -> dict[str, Any]:
+ out: dict[str, Any] = {"version": self.version, "receiptId": self.receipt_id,
+ "intent": self.intent, "route": self.route}
+ if self.source:
+ out["source"] = self.source
+ if self.destination:
+ out["destination"] = self.destination
+ if self.delivery_verification:
+ out["deliveryVerification"] = self.delivery_verification
+ return out
+
+ def to_json(self) -> str:
+ return json.dumps(self.to_dict(), indent=2, sort_keys=True)
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "Checkpoint":
+ if not isinstance(data, dict) or data.get("version") != CHECKPOINT_VERSION \
+ or not isinstance(data.get("intent"), dict) or not isinstance(data.get("route"), dict):
+ raise CheckpointInvalidError(
+ "Bridge checkpoint format is invalid or unsupported: expected version 1 "
+ "with 'intent' and 'route' objects")
+ source = data.get("source") or None
+ destination = data.get("destination") or None
+ receipt_id = data.get("receiptId") or _derive_id(source, destination)
+ return cls(version=1, receipt_id=receipt_id, intent=dict(data["intent"]),
+ route=dict(data["route"]), source=source, destination=destination,
+ delivery_verification=data.get("deliveryVerification") or None)
+
+ @classmethod
+ def from_json(cls, text: str) -> "Checkpoint":
+ try:
+ return cls.from_dict(json.loads(text))
+ except json.JSONDecodeError as exc:
+ raise CheckpointInvalidError(f"Bridge checkpoint is not valid JSON: {exc}") from exc
+
+
+def _derive_id(source: dict[str, Any] | None, destination: dict[str, Any] | None) -> str:
+ """veil-shaped checkpoints have no receiptId: use the most recent transaction id."""
+ for container, keys in ((destination, ("transactionId", "preparedTransaction")),
+ (source, ("preparedTransaction", "transactionId"))):
+ for key in keys:
+ value = (container or {}).get(key)
+ if isinstance(value, dict) and value.get("transactionId"):
+ return str(value["transactionId"])
+ if isinstance(value, str) and value:
+ return value
+ approvals = (source or {}).get("approvalTransactionIds") or []
+ if approvals:
+ return str(approvals[-1])
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains no submitted or prepared transaction to identify it by")
+
+
+def _validated_prepared(value: Any, what: str) -> str | None:
+ if value is None:
+ return None
+ if not isinstance(value, str) or not value:
+ raise CheckpointInvalidError(f"Bridge receipt contains an invalid {what}")
+ return value
+
+
+def create_checkpoint(plan: Plan, receipt: Receipt, registry: Registry) -> Checkpoint:
+ """Reduce *receipt* to the documented recovery fields (allowlist, brief §2.10).
+
+ Raises :class:`CheckpointInvalidError` when the receipt belongs to another
+ route/protocol or carries malformed approval ids, prepared transactions,
+ Solana blockhash lifetime, or delivery-verification state.
+ """
+ state = receipt.protocol_state
+ if receipt.protocol != plan.protocol or state.get("routeId") != plan.route_id:
+ raise CheckpointInvalidError("Bridge receipt does not match the prepared route")
+
+ raw_approvals = state.get("approvalTxIds")
+ if raw_approvals is not None and (not isinstance(raw_approvals, list)
+ or any(not isinstance(v, str) for v in raw_approvals)):
+ raise CheckpointInvalidError("Bridge receipt contains invalid approval transaction identifiers")
+ approvals = list(raw_approvals or [])
+
+ source_sender = state.get("sourceSender")
+ if source_sender is not None and not isinstance(source_sender, str):
+ raise CheckpointInvalidError("Bridge receipt contains an invalid source sender")
+ sender = plan.sender or source_sender
+
+ prepared = _validated_prepared(state.get("preparedTransaction"), "prepared transaction")
+ prepared_destination = _validated_prepared(state.get("preparedDestinationTransaction"),
+ "prepared destination transaction")
+
+ blockhash = state.get("blockhash")
+ last_valid = state.get("lastValidBlockHeight")
+ if (blockhash is not None or last_valid is not None) and (
+ not isinstance(blockhash, str) or not blockhash
+ or not isinstance(last_valid, str) or not _DIGITS.match(last_valid)):
+ raise CheckpointInvalidError("Bridge receipt contains an invalid Solana blockhash lifetime")
+
+ before = state.get("destinationBalanceBeforeAtomic")
+ expected = state.get("expectedDestinationIncreaseAtomic")
+ if (before is not None or expected is not None) and (
+ not isinstance(before, str) or not _DIGITS.match(before)
+ or not isinstance(expected, str) or not _DIGITS.match(expected)):
+ raise CheckpointInvalidError(
+ "Bridge receipt contains invalid destination balance verification state")
+
+ source: dict[str, Any] | None = None
+ if approvals or receipt.source_tx_id or prepared:
+ source = {}
+ if approvals:
+ source["approvalTransactionIds"] = approvals
+ if receipt.source_tx_id:
+ source["transactionId"] = receipt.source_tx_id
+ if isinstance(state.get("hookData"), str):
+ source["hookData"] = state["hookData"]
+ if isinstance(blockhash, str) and isinstance(last_valid, str):
+ source["blockhash"] = blockhash
+ source["lastValidBlockHeight"] = last_valid
+ if prepared is not None:
+ source["preparedTransaction"] = {"transactionId": receipt.id,
+ "serializedTransaction": prepared}
+
+ destination: dict[str, Any] | None = None
+ if receipt.destination_tx_id or prepared_destination is not None:
+ destination = {}
+ if receipt.destination_tx_id:
+ destination["transactionId"] = receipt.destination_tx_id
+ if prepared_destination is not None:
+ destination["preparedTransaction"] = {"transactionId": receipt.id,
+ "serializedTransaction": prepared_destination}
+
+ src_asset = registry.asset(plan.source_asset_id)
+ dst_asset = registry.asset(plan.destination_asset_id)
+ intent: dict[str, Any] = {
+ "source": {"chain": src_asset.chain_id, "asset": src_asset.key},
+ "destination": {"chain": dst_asset.chain_id, "asset": dst_asset.key},
+ "bridgeProtocol": plan.protocol,
+ "amount": plan.amount,
+ "recipient": plan.recipient,
+ }
+ if sender:
+ intent["sender"] = sender
+ if dst_asset.locator is not None and dst_asset.locator.kind == "aleo-program":
+ intent["mintMode"] = plan.mint_mode
+
+ verification = ({"balanceBeforeAtomic": before, "expectedIncreaseAtomic": expected}
+ if isinstance(before, str) and isinstance(expected, str) else None)
+ return Checkpoint(version=CHECKPOINT_VERSION, receipt_id=receipt.id, intent=intent,
+ route={"id": plan.route_id, "registryVersion": plan.registry_version},
+ source=source, destination=destination, delivery_verification=verification)
+
+
+@runtime_checkable
+class CheckpointStore(Protocol):
+ """Where checkpoints live between processes. Implement all four methods."""
+
+ def save(self, checkpoint: Checkpoint) -> None: ...
+ def load(self, checkpoint_id: str) -> Checkpoint | None: ...
+ def list(self) -> list[Checkpoint]: ...
+ def delete(self, checkpoint_id: str) -> None: ...
+
+
+_UNSAFE = re.compile(r"[^A-Za-z0-9_-]")
+
+
+class FileCheckpointStore:
+ """One ``.json`` per receipt id under *directory*; mode 0600; atomic rename.
+
+ ``list()`` returns oldest-first by mtime. ``delete()`` of a missing id is a
+ no-op. Ids are sanitized for the filesystem; the stored ``receiptId`` keeps
+ the original.
+ """
+
+ def __init__(self, directory: Path | str) -> None:
+ self.directory = Path(directory)
+ self.directory.mkdir(parents=True, exist_ok=True)
+
+ def _path(self, checkpoint_id: str) -> Path:
+ safe = _UNSAFE.sub("_", checkpoint_id) or "_"
+ return self.directory / f"{safe}.json"
+
+ def save(self, checkpoint: Checkpoint) -> None:
+ target = self._path(checkpoint.id)
+ fd, tmp = tempfile.mkstemp(dir=self.directory, prefix=".", suffix=".tmp")
+ try:
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
+ fh.write(checkpoint.to_json())
+ fh.write("\n")
+ os.chmod(tmp, 0o600)
+ os.replace(tmp, target)
+ except BaseException:
+ try:
+ os.unlink(tmp)
+ except FileNotFoundError:
+ pass
+ raise
+
+ def load(self, checkpoint_id: str) -> Checkpoint | None:
+ path = self._path(checkpoint_id)
+ if not path.exists():
+ return None
+ return Checkpoint.from_json(path.read_text(encoding="utf-8"))
+
+ def list(self) -> list[Checkpoint]:
+ paths = sorted((p for p in self.directory.glob("*.json") if not p.name.startswith(".")),
+ key=lambda p: (p.stat().st_mtime_ns, p.name))
+ return [Checkpoint.from_json(p.read_text(encoding="utf-8")) for p in paths]
+
+ def delete(self, checkpoint_id: str) -> None:
+ try:
+ self._path(checkpoint_id).unlink()
+ except FileNotFoundError:
+ pass
+
+
+__all__ = ["Checkpoint", "CheckpointStore", "FileCheckpointStore", "create_checkpoint"]
diff --git a/bridge-sdk/python/aleo_bridge/circle.py b/bridge-sdk/python/aleo_bridge/circle.py
new file mode 100644
index 0000000..be07c4c
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/circle.py
@@ -0,0 +1,57 @@
+"""Circle xReserve attester — read-only HTTP (port of veil ``getAttestation``). Never signs or moves funds."""
+from __future__ import annotations
+
+from typing import Any
+
+import requests
+
+from . import encoding as enc
+from ._keccak import keccak256
+from .errors import AttestationError, ConfigurationError, InvalidRecipientError
+from .types import Attestation
+
+
+class CircleClient:
+ """``GET {base_url}/{messageHash}``: 404 → ``None`` (pending); 200 → a verified :class:`Attestation`."""
+
+ def __init__(self, base_url: str, session: Any = None, timeout: float = 30.0) -> None:
+ if not isinstance(base_url, str) or not base_url.startswith("https://"):
+ raise ConfigurationError(f"Circle attestation base URL must start with https://, got {base_url!r}")
+ self.base_url = base_url.rstrip("/")
+ self.timeout = timeout
+ self._session = session if session is not None else requests.Session()
+
+ def get_attestation(self, message_hash_hex: str) -> Attestation | None:
+ try:
+ digest = enc.hex_to_bytes(message_hash_hex, 32)
+ except (ValueError, InvalidRecipientError) as exc:
+ raise AttestationError(f"Circle attestation lookup needs a 32-byte message hash, got {message_hash_hex!r}") from exc
+ try:
+ response = self._session.get(f"{self.base_url}/{enc.to_hex(digest)}", timeout=self.timeout)
+ except requests.exceptions.RequestException as exc:
+ raise AttestationError(f"Circle attester request failed: {exc}") from exc
+ if response.status_code == 404:
+ return None
+ if response.status_code != 200:
+ raise AttestationError(f"Circle attester request failed with HTTP {response.status_code}")
+ try:
+ body = response.json()
+ except ValueError as exc:
+ raise AttestationError(f"Circle attester request failed: {exc}") from exc
+ value = body.get("attestation") if isinstance(body, dict) else None
+ if not isinstance(value, dict):
+ raise AttestationError("Circle attester returned an invalid response (no attestation object)")
+ try:
+ payload = enc.hex_to_bytes(value["payload"], enc.XRESERVE_PAYLOAD_BYTES)
+ signature = enc.hex_to_bytes(value["attestation"], enc.HOOK_DATA_BYTES)
+ echoed = enc.hex_to_bytes(value["messageHash"], 32)
+ except (KeyError, TypeError, ValueError, InvalidRecipientError) as exc:
+ raise AttestationError("Circle attester returned an invalid response (payload/attestation/messageHash)") from exc
+ if echoed != digest:
+ raise AttestationError("Circle attester echoed a different message hash than requested")
+ if keccak256(payload) != digest:
+ raise AttestationError("Circle attestation payload does not match the requested message hash")
+ return Attestation(payload=payload, message_hash=digest, attestation=signature, status="complete")
+
+
+__all__ = ["CircleClient"]
diff --git a/bridge-sdk/python/aleo_bridge/client.py b/bridge-sdk/python/aleo_bridge/client.py
new file mode 100644
index 0000000..479b041
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/client.py
@@ -0,0 +1,437 @@
+"""Bridge — the web3.py-style client for moving assets to and from Aleo.
+
+ bridge = Bridge(aleo) # Aleo legs: hyperlane / xreserve / privacy / freezelist
+ bridge = Bridge.from_env() # BRIDGE_PRIVATE_KEY, ALEO_ENDPOINT, ALEO_NETWORK, …
+ bridge = Bridge.from_profile() # $ALEO_BRIDGE_HOME or ~/.aleo-bridge
+
+Reads return values; Aleo writes return an AleoCall — nothing touches the network until a verb runs.
+Ethereum/Solana connections (plans 2/3), lifecycle verbs and checkpoints (plan 4) plug into the
+constructor parameters reserved here.
+"""
+from __future__ import annotations
+
+import os
+import re
+from typing import TYPE_CHECKING, Any, Callable
+
+from . import lifecycle as _lifecycle
+from ._calls import AleoCall
+from .errors import BridgeError, ConfigurationError
+from .eth import Ethereum, EthModule
+from .freezelist import FreezeList
+from .hyperlane import HyperlaneModule
+from .privacy import PrivacyModule
+from .profile import DEFAULT_ENDPOINT, Profile
+from .registry import DEFAULT_REGISTRY, Asset, Chain, Registry, validate_registry
+from .sol import Solana, SolModule
+from .types import BridgeStatus, ChainStatus, PrivacyReceipt
+from .units import format_decimal_amount, parse_decimal_amount
+from .xreserve import XReserveModule
+
+if TYPE_CHECKING: # pragma: no cover
+ from .types import Progress
+
+NETWORKS = ("mainnet", "testnet")
+BALANCE_MAPPING = "balances"
+_UINT_LITERAL_RE = re.compile(r"^(\d+)u\d+$")
+
+
+def parse_uint_literal(value: str) -> int:
+ """``"2392443u64"`` → 2392443 (any ``uN`` suffix)."""
+ match = _UINT_LITERAL_RE.match(value.strip())
+ if not match:
+ raise ConfigurationError(f"Expected an unsigned Aleo integer literal, got {value!r}")
+ return int(match.group(1))
+
+
+def balance_program(asset: Asset) -> str | None:
+ """Program whose ``balances`` mapping holds *asset*'s public balance (None for ALEO credits / no locator)."""
+ # Verified on chain 2026-09-17: the warp/xreserve programs mint/burn via arc20_.aleo's
+ # mint_public/burn_public, so this IS the ledger transfer_remote (and xreserve burns) spend.
+ if asset.locator is None or asset.locator.kind != "aleo-program" or asset.locator.value == "credits.aleo":
+ return None
+ return asset.privacy.program if asset.privacy is not None else asset.locator.value
+
+
+def build_aleo(endpoint: str, network: str, private_key: str, *, api_key: str | None = None,
+ consumer_id: str | None = None) -> Any:
+ """An ``aleo.Aleo`` facade bound to *endpoint*/*network* with *private_key* as the default account (local only)."""
+ from aleo import Aleo, HTTPProvider
+ kwargs: dict[str, Any] = {"network": network}
+ if api_key:
+ kwargs["api_key"] = api_key
+ if consumer_id:
+ kwargs["consumer_id"] = consumer_id
+ aleo = Aleo(HTTPProvider(endpoint, **kwargs))
+ aleo.default_account = aleo.account.from_private_key(private_key)
+ return aleo
+
+
+def ethereum_from_env() -> Any:
+ """``Ethereum(ETHEREUM_RPC_URL, private_key=EVM_PRIVATE_KEY)`` or None; both variables or neither."""
+ return Ethereum.from_env()
+
+
+def _coerce_ethereum(value: Any) -> Ethereum | None:
+ """Accept an ``Ethereum`` or a bare ``web3.Web3`` (wrapped; signs only via ``eth.default_account``)."""
+ if value is None or isinstance(value, Ethereum):
+ return value
+ if hasattr(value, "eth") and hasattr(value, "provider"):
+ return Ethereum(w3=value)
+ raise ConfigurationError("ethereum= must be an aleo_bridge.Ethereum connection or a web3.Web3 instance")
+
+
+def _coerce_solana(value: Any) -> Solana | None:
+ """Accept a ``Solana``, an RPC URL string, or a bare solana-py ``Client`` (wrapped read-only).
+
+ Mirrors :func:`_coerce_ethereum`: anything else is a configuration mistake caught here rather
+ than as an ``AttributeError`` from the first RPC read.
+ """
+ if value is None or isinstance(value, Solana):
+ return value
+ if isinstance(value, str):
+ return Solana(rpc_url=value)
+ if callable(getattr(value, "get_latest_blockhash", None)) and callable(getattr(value, "get_account_info", None)):
+ return Solana(client=value) # spec §3: a bare client is a read-only connection
+ raise ConfigurationError(
+ "solana= must be an aleo_bridge.Solana connection, an RPC URL string, or a solana-py Client "
+ "(an object with get_latest_blockhash and get_account_info)")
+
+
+def solana_from_env() -> Any:
+ """``Solana.from_env()``: SOLANA_PRIVATE_KEY/BRIDGE_SOLANA_PRIVATE_KEY (+ SOLANA_RPC_URL/
+ BRIDGE_LIVE_SOLANA_RPC_URL) or None; a key alone signs, a URL alone is read-only, neither → None."""
+ return Solana.from_env()
+
+
+def checkpoints_from_env() -> Any:
+ """``FileCheckpointStore(BRIDGE_CHECKPOINT_DIR)`` or None."""
+ directory = os.environ.get("BRIDGE_CHECKPOINT_DIR")
+ if not directory:
+ return None
+ from .checkpoint import FileCheckpointStore
+ return FileCheckpointStore(directory)
+
+
+def _checkpoints_for_profile(profile: Profile) -> Any:
+ from .checkpoint import FileCheckpointStore
+ return FileCheckpointStore(profile.checkpoint_dir)
+
+
+class Bridge:
+ """Typed bridge client over the Aleo facade (and, from plans 2/3, Ethereum/Solana connections)."""
+
+ def __init__(self, aleo: Any, *, ethereum: Any = None, solana: Any = None, environment: str | None = None,
+ registry: Registry | None = None, checkpoints: Any = None) -> None:
+ network = getattr(aleo, "network_name", None)
+ if network not in NETWORKS:
+ raise ConfigurationError(f"The aleo facade must report network_name mainnet or testnet, got {network!r}")
+ environment = environment if environment is not None else network
+ if environment not in NETWORKS:
+ raise ConfigurationError(f"environment must be mainnet or testnet, got {environment!r}")
+ if environment != network:
+ raise ConfigurationError(f"Bridge environment {environment!r} does not match the facade network {network!r}")
+ self.aleo = aleo
+ self.environment: str = environment
+ self.network: str = network
+ self.registry: Registry = validate_registry(registry if registry is not None else DEFAULT_REGISTRY)
+ if not self.registry.chains(environment=environment):
+ raise ConfigurationError(f"Registry {self.registry.version} has no chains for {environment}")
+ self.checkpoints = checkpoints
+ self.ethereum: Ethereum | None = _coerce_ethereum(ethereum)
+ self.solana: Solana | None = _coerce_solana(solana)
+ solana = self.solana
+ self._sol: SolModule | None = SolModule(self, solana) if solana is not None else None
+ self.profile: Profile | None = None
+ self._eth: EthModule | None = None
+ self._programs: dict[str, Any] = {}
+ self.hyperlane = HyperlaneModule(self)
+ self.xreserve = XReserveModule(self)
+ self.freezelist = FreezeList(self)
+ self.privacy = PrivacyModule(self)
+
+ def __repr__(self) -> str:
+ return f"Bridge(environment={self.environment!r}, registry={self.registry.version!r})"
+
+ # ── side-chain namespaces (plan 3 supplies the Solana module) ──
+ @property
+ def eth(self) -> EthModule:
+ """Ethereum-origin actions (Hyperlane transferRemote, xReserve deposit, status, recovery)."""
+ if self.ethereum is None:
+ raise ConfigurationError(
+ "Pass ethereum=Ethereum(...) to Bridge(...) or set EVM_PRIVATE_KEY + ETHEREUM_RPC_URL")
+ if self._eth is None:
+ self._eth = EthModule(self, self.ethereum)
+ return self._eth
+
+ @property
+ def sol(self) -> SolModule:
+ """Solana-origin module (spec §6). Requires a Solana connection."""
+ if self._sol is None:
+ raise ConfigurationError(
+ "Solana is not configured: pass solana=Solana(rpc_url, private_key=...) or a solana-py Client to Bridge(), "
+ "or set SOLANA_PRIVATE_KEY (and optionally SOLANA_RPC_URL) for Bridge.from_env()")
+ return self._sol
+
+ # ── identity / registry helpers ──
+ def aleo_chain(self) -> Chain:
+ chains = [c for c in self.registry.chains(environment=self.environment) if c.family == "aleo"]
+ if len(chains) != 1:
+ raise ConfigurationError(f"Registry must define exactly one Aleo chain for {self.environment}")
+ return chains[0]
+
+ def solana_chain(self) -> Chain | None:
+ """The environment's Solana chain, or None — only mainnet has one."""
+ chains = [c for c in self.registry.chains(environment=self.environment) if c.family == "solana"]
+ return chains[0] if chains else None
+
+ def aleo_address(self) -> str:
+ account = getattr(self.aleo, "default_account", None)
+ if not account:
+ raise ConfigurationError("aleo.default_account is not set; assign aleo.account.from_private_key(...) first")
+ return str(account.address)
+
+ def to_atomic(self, amount: Any, asset: Any) -> int:
+ return parse_decimal_amount(amount, self.registry.asset(asset).decimals)
+
+ def from_atomic(self, atomic: int, asset: Any) -> str:
+ return format_decimal_amount(atomic, self.registry.asset(asset).decimals)
+
+ # ── facade seams used by every module ──
+ def program(self, program_id: str) -> Any:
+ """Facade ``Program`` for *program_id*, fetched once per client."""
+ if program_id not in self._programs:
+ self._programs[program_id] = self.aleo.programs.get(program_id)
+ return self._programs[program_id]
+
+ def mapping_value(self, program_id: str, mapping: str, key: str) -> str | None:
+ """Mapping value as a string, or None when the key is absent/null, or *program_id* does not exist.
+
+ A missing program is reported through the return value here, not an exception, so status()/freezelist
+ reads over a program that may not be deployed (yet) degrade to "no data" instead of raising. Callers
+ that want the error can still get it from ``program(program_id)`` directly.
+ """
+ from aleo.facade.errors import ProgramNotFound
+ try:
+ value = self.program(program_id).mapping(mapping).get(key)
+ except ProgramNotFound:
+ return None
+ if value is None:
+ return None
+ text = str(value).strip().strip('"')
+ return None if text in ("", "null", "None") else text
+
+ def _import_sources(self, program_id: str) -> dict[str, str]:
+ """``{program_id: source}`` for every transitive import (dependencies first) and the root last."""
+ ordered: dict[str, str] = {}
+
+ def visit(pid: str) -> None:
+ if pid in ordered:
+ return
+ program = self.program(pid)
+ for dep in program.imports:
+ visit(str(dep))
+ ordered[pid] = str(program.source)
+
+ visit(program_id)
+ return ordered
+
+ def _call(self, program_id: str, function: str, inputs: list[str], build_result: Callable[[str, list[str]], Any]) -> AleoCall:
+ bound = self.program(program_id).functions[function](*inputs)
+ return AleoCall(self.aleo, bound, build_result, imports=self._import_sources(program_id))
+
+ # ── privacy shortcuts ──
+ def shield(self, asset: Any, *, amount: Any = None, amount_atomic: int | None = None,
+ recipient: str | None = None) -> AleoCall[PrivacyReceipt]:
+ return self.privacy.shield(asset, amount=amount, amount_atomic=amount_atomic, recipient=recipient)
+
+ def unshield(self, asset: Any, *, amount: Any = None, amount_atomic: int | None = None, record: str | None = None,
+ merkle_proof: str | None = None, recipient: str | None = None) -> AleoCall[PrivacyReceipt]:
+ return self.privacy.unshield(asset, amount=amount, amount_atomic=amount_atomic, record=record,
+ merkle_proof=merkle_proof, recipient=recipient)
+
+ # ── status ──
+ def _public_balance(self, asset: Asset, address: str) -> int:
+ if asset.locator is not None and asset.locator.value == "credits.aleo":
+ value = self.mapping_value("credits.aleo", "account", address)
+ else:
+ program = balance_program(asset)
+ if program is None:
+ return 0
+ value = self.mapping_value(program, BALANCE_MAPPING, address)
+ return parse_uint_literal(value) if value is not None else 0
+
+ def _aleo_chain_status(self) -> ChainStatus:
+ chain = self.aleo_chain()
+ account = getattr(self.aleo, "default_account", None)
+ address = str(account.address) if account else None
+ balances = {asset.id: (self._public_balance(asset, address) if address else 0)
+ for asset in self.registry.assets(chain=chain.id)}
+ return ChainStatus(chain.id, address, address is not None, balances)
+
+ def status(self) -> BridgeStatus:
+ """Read-only re-orientation: addresses and public balances of every registry asset per configured chain.
+ Plan 4 fills ``pending`` from the checkpoint store."""
+ chains = [self._aleo_chain_status()]
+ if self.ethereum is not None:
+ chains.append(self.eth.chain_status())
+ solana_chain = self.solana_chain()
+ if self.solana is not None and solana_chain is not None:
+ # Chain id and asset id come from the registry, not literals: a testnet client (no Solana
+ # chain at all) reports no Solana row rather than one naming a chain this environment lacks.
+ native = next((a for a in self.registry.assets(chain=solana_chain.id) if a.kind == "native"), None)
+ balances = ({native.id: self.sol.balance()}
+ if native is not None and self.solana.address is not None else {})
+ chains.append(ChainStatus(chain_id=solana_chain.id, address=self.solana.address,
+ can_sign=self.solana.can_sign, balances=balances))
+ pending: list["Progress"] = []
+ return BridgeStatus(environment=self.environment, registry_version=self.registry.version,
+ chains=chains, pending=pending)
+
+ # ── Tier 1: the lifecycle ──────────────────────────────────────────────
+
+ def quote(self, source, destination, *, amount=None, amount_atomic=None, recipient: str,
+ sender: str | None = None, protocol: str | None = None, mint_mode: str = "public",
+ secret_nonce: str = "0scalar"):
+ """Price a transfer and get the plan that ``execute`` takes. Nothing is signed.
+
+ ``source`` / ``destination`` are ``"chain/key"`` strings or ``(chain, key)``
+ tuples (``"ethereum/usdc"``, ``"aleo/usdcx"``); give exactly one of
+ ``amount`` (human units, str) or ``amount_atomic`` (int). ``recipient`` is
+ the destination-chain address. ``mint_mode`` (xReserve into Aleo only):
+ ``"public"`` balance, ``"record"`` minted by the relayer, or ``"private"``
+ — you finish it yourself with ``complete`` and must keep ``secret_nonce``.
+ Returns a kind-specific ``Quote`` (``quote.kind`` in evm-hyperlane /
+ solana-hyperlane / aleo-hyperlane / evm-xreserve / aleo-xreserve) with
+ ``fees`` and ``amount_out`` in human units and ``quote.plan``. Show the
+ user fees + amount before ``execute``.
+ """
+ return _lifecycle.quote(self, source=source, destination=destination, amount=amount,
+ amount_atomic=amount_atomic, recipient=recipient, sender=sender,
+ protocol=protocol, mint_mode=mint_mode, secret_nonce=secret_nonce)
+
+ def execute(self, plan, *, on_checkpoint=None, proving: str = "delegate", mode: str | None = None,
+ record: str | None = None, merkle_proof: str | None = None,
+ gas_payment_microcredits: int | None = None, secret_nonce: str | None = None,
+ poll_seconds: float = 1.0, timeout_seconds: float = 120.0):
+ """Commit funds on the source chain for ``quote.plan``; returns ``Progress``.
+
+ Runs approval(s) → deposit / dispatch / burn, emitting a ``Checkpoint`` to
+ ``on_checkpoint`` (and the bound store) at every boundary — including
+ AFTER proving and BEFORE broadcast for Aleo legs, so a crash there is
+ resumable without proving twice. ``proving`` is ``"delegate"`` (DPS) or
+ ``"local"``; ``mode`` is ``"caller"|"signer"`` (Aleo Hyperlane) or
+ ``"private"|"public"|"public-as-signer"`` (Aleo xReserve burn, default
+ private; ``record``/``merkle_proof`` optional — the SDK selects a record
+ and computes the exclusion proof). The Hyperlane hook payment is
+ re-quoted right before proving unless ``gas_payment_microcredits`` is
+ pinned. Irreversible once the source step is broadcast: afterwards use
+ ``wait`` / ``recover``, never ``execute`` again.
+ """
+ return _lifecycle.execute(self, plan, on_checkpoint=on_checkpoint, proving=proving, mode=mode,
+ record=record, merkle_proof=merkle_proof,
+ gas_payment_microcredits=gas_payment_microcredits, secret_nonce=secret_nonce,
+ poll_seconds=poll_seconds, timeout_seconds=timeout_seconds)
+
+ def get_status(self, plan, receipt):
+ """One status refresh (no polling, no signing); returns the same receipt when nothing changed."""
+ return _lifecycle.get_status(self, plan, receipt)
+
+ def wait(self, progress, *, until=None, poll_seconds: float = 15.0, timeout_seconds: float = 1200.0,
+ on_update=None, on_error=None, max_consecutive_errors: int = 5):
+ """Poll until the transfer finishes or needs you: stops at ``progress.next``
+ in resume / complete / done / failed, or at any status in ``until``.
+
+ A ``PollingTimeoutError`` is NOT a failure — the transfer is still in
+ flight; call ``wait`` again or ``recover`` later. ``on_update`` receives
+ each changed ``Progress``. A transient error (flaky RPC/HTTP transport)
+ is retried up to ``max_consecutive_errors`` times, calling ``on_error``
+ on each tolerated retry; a non-transient error propagates immediately.
+ """
+ return _lifecycle.wait(self, progress, until=until, poll_seconds=poll_seconds,
+ timeout_seconds=timeout_seconds, on_update=on_update, on_error=on_error,
+ max_consecutive_errors=max_consecutive_errors)
+
+ def recover(self, checkpoint):
+ """Rebuild ``Progress`` from a saved checkpoint (``Checkpoint``, dict or JSON) — reads only.
+
+ Re-resolves the route from the live registry and reads chain state once;
+ ``progress.next`` then says what to do: ``wait``, ``resume``, ``complete``,
+ ``done`` or ``failed``.
+ """
+ return _lifecycle.recover(self, checkpoint)
+
+ def resume(self, progress, *, on_checkpoint=None, secret_nonce: str | None = None,
+ poll_seconds: float = 1.0, timeout_seconds: float = 120.0, proving: str = "delegate"):
+ """Finish an interrupted source submission (``progress.next == "resume"``).
+
+ Rebroadcasts the identical proved Aleo transaction (a duplicate response is
+ success) or, on EVM, re-scans history and only then authorizes the single
+ missing deposit/dispatch. Never repeats a confirmed step.
+ """
+ return _lifecycle.resume(self, progress, on_checkpoint=on_checkpoint, secret_nonce=secret_nonce,
+ poll_seconds=poll_seconds, timeout_seconds=timeout_seconds, proving=proving)
+
+ def complete(self, progress, *, secret_nonce: str, on_checkpoint=None, proving: str = "delegate"):
+ """Submit the private USDCx mint (``progress.next == "complete"``).
+
+ Requires the same ``secret_nonce`` given to ``execute``; the SDK never
+ stored it. Submits exactly one ``private_mint`` and returns
+ ``DESTINATION_CONFIRMING`` progress to ``wait`` on.
+ """
+ return _lifecycle.complete(self, progress, secret_nonce=secret_nonce, on_checkpoint=on_checkpoint,
+ proving=proving)
+
+ def pending(self) -> list:
+ """The in-flight transfers of this profile — every checkpoint in the bound store,
+ reconstructed offline (:func:`lifecycle.progress_from_checkpoint`): no network read, so one
+ unreachable chain can never hide the others. A malformed checkpoint yields a ``Progress``
+ with ``next == "failed"`` and ``error`` set instead of raising; call ``wait()``/``recover()``
+ on any entry to refresh it against live chain state.
+ """
+ store = self.checkpoints
+ if store is None:
+ return []
+ out = []
+ for cp in store.list():
+ try:
+ out.append(_lifecycle.progress_from_checkpoint(self.registry, cp))
+ except BridgeError:
+ continue # no Plan could be rebuilt at all (bad format/version/route) — nothing to report
+ return out
+
+ # ── constructors ──
+ @classmethod
+ def from_env(cls, **overrides: Any) -> "Bridge":
+ """Everything from the environment (spec §3.3); writes nothing to disk. Overrides: ethereum, solana, registry, checkpoints."""
+ unexpected = set(overrides) - {"ethereum", "solana", "registry", "checkpoints"}
+ if unexpected:
+ raise TypeError(f"Bridge.from_env() got unexpected overrides: {sorted(unexpected)}")
+ private_key = os.environ.get("BRIDGE_PRIVATE_KEY")
+ if not private_key:
+ raise ConfigurationError("BRIDGE_PRIVATE_KEY is required (an APrivateKey1... string)")
+ aleo = build_aleo(os.environ.get("ALEO_ENDPOINT", DEFAULT_ENDPOINT), os.environ.get("ALEO_NETWORK", "mainnet"),
+ private_key, api_key=os.environ.get("ALEO_API_KEY"), consumer_id=os.environ.get("ALEO_CONSUMER_ID"))
+ ethereum = overrides["ethereum"] if "ethereum" in overrides else ethereum_from_env()
+ solana = overrides["solana"] if "solana" in overrides else solana_from_env()
+ checkpoints = overrides["checkpoints"] if "checkpoints" in overrides else checkpoints_from_env()
+ return cls(aleo, ethereum=ethereum, solana=solana, registry=overrides.get("registry"), checkpoints=checkpoints)
+
+ @classmethod
+ def from_profile(cls, home: Any = None, *, network: str | None = None, endpoint: str | None = None,
+ ethereum: Any = None, solana: Any = None) -> "Bridge":
+ """The client for the local profile (spec §3.4), created on first use. *network*/*endpoint* apply only when
+ creating. Side-chain connections come from the arguments or the same env variables as ``from_env``."""
+ kwargs = {k: v for k, v in (("network", network), ("endpoint", endpoint)) if v is not None}
+ profile = Profile.load_or_create(home, **kwargs)
+ aleo = build_aleo(profile.endpoint, profile.network, profile.private_key,
+ api_key=os.environ.get("ALEO_API_KEY"), consumer_id=os.environ.get("ALEO_CONSUMER_ID"))
+ bridge = cls(aleo, ethereum=ethereum if ethereum is not None else ethereum_from_env(),
+ solana=solana if solana is not None else solana_from_env(),
+ checkpoints=_checkpoints_for_profile(profile))
+ bridge.profile = profile
+ return bridge
+
+
+__all__ = ["BALANCE_MAPPING", "Bridge", "balance_program", "build_aleo", "checkpoints_from_env", "ethereum_from_env",
+ "parse_uint_literal", "solana_from_env"]
diff --git a/bridge-sdk/python/aleo_bridge/encoding.py b/bridge-sdk/python/aleo_bridge/encoding.py
new file mode 100644
index 0000000..725322a
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/encoding.py
@@ -0,0 +1,289 @@
+"""Pure wire encoders shared by every route family (port of veil utils/xreserve.ts, hyperlane.ts,
+hyperlaneDelivery.ts). Nothing here touches the network; only ``xreserve_hook_data`` (private mode)
+and ``aleo_program_address`` load the ``aleo.`` bindings.
+
+Spacing rules (invariant 8): ``u8_array_literal`` joins with ``","`` and NO space, every other
+struct/array literal joins with ``", "``.
+"""
+from __future__ import annotations
+
+import re
+from typing import Any
+
+from ._base58 import b58decode
+from ._keccak import keccak256
+from .errors import AttestationError, ConfigurationError, InvalidAmountError, InvalidRecipientError
+
+BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+BECH32M_CONST = 0x2BC830A3
+_BECH32_GENERATORS = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
+_HRP = "aleo"
+_EVM_ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
+_SCALAR_RE = re.compile(r"^(0|[1-9][0-9]*)scalar$")
+NETWORKS = ("mainnet", "testnet")
+MINT_MODES = ("public", "record", "private")
+HOOK_DATA_BYTES = 65
+XRESERVE_PAYLOAD_BYTES = 305
+_PAYLOAD_HEADER = bytes.fromhex("5a2e0acd00000001")
+_HOOK_LENGTH_FIELD = bytes.fromhex("00000041")
+
+
+# ── Generic byte helpers ──────────────────────────────────────────────────────
+
+def hex_to_bytes(value: "str | bytes | bytearray | memoryview", expected_len: "int | None" = None) -> bytes:
+ """``0x``-prefixed (or bare) hex, or bytes, to ``bytes``; optionally assert the width."""
+ if isinstance(value, str):
+ text = value[2:] if value[:2] in ("0x", "0X") else value
+ try:
+ data = bytes.fromhex(text)
+ except ValueError as exc:
+ raise InvalidRecipientError(f"Not hexadecimal: {value!r}") from exc
+ else:
+ data = bytes(value)
+ if expected_len is not None and len(data) != expected_len:
+ raise InvalidRecipientError(f"Expected {expected_len} bytes, got {len(data)}")
+ return data
+
+
+def to_hex(data: "bytes | bytearray | memoryview") -> str:
+ return "0x" + bytes(data).hex()
+
+
+def network_module(network: str) -> Any:
+ """``aleo.mainnet`` / ``aleo.testnet`` for *network*; ``ConfigurationError`` otherwise."""
+ if network not in NETWORKS:
+ raise ConfigurationError(f"network must be one of {NETWORKS}, got {network!r}")
+ import aleo # noqa: WPS433 — the bindings are a runtime dependency, imported lazily
+ return getattr(aleo, network)
+
+
+def field_bytes_le(value: Any) -> bytes:
+ """``Field/Address.to_bytes_le()`` returns ``list[int]`` at runtime; normalise to bytes."""
+ return bytes(value.to_bytes_le())
+
+
+def validate_scalar(secret_nonce: str) -> str:
+ if not isinstance(secret_nonce, str) or not _SCALAR_RE.match(secret_nonce):
+ raise ConfigurationError(
+ f"secret_nonce must be a non-negative Aleo scalar literal such as 0scalar, got {secret_nonce!r}")
+ return secret_nonce
+
+
+# ── Aleo bech32m ──────────────────────────────────────────────────────────────
+
+def _bech32_polymod(values: list[int]) -> int:
+ chk = 1
+ for v in values:
+ top = chk >> 25
+ chk = ((chk & 0x1FFFFFF) << 5) ^ v
+ for i in range(5):
+ if (top >> i) & 1:
+ chk ^= _BECH32_GENERATORS[i]
+ return chk
+
+
+def _hrp_expand() -> list[int]:
+ return [ord(c) >> 5 for c in _HRP] + [0] + [ord(c) & 31 for c in _HRP]
+
+
+def aleo_address_to_bytes32(address: str) -> bytes:
+ """Decode a checksummed ``aleo1…`` bech32m address into its 32 payload bytes (xReserve/Hyperlane form)."""
+ try:
+ if not isinstance(address, str) or not address.startswith("aleo1") or len(address) != 63:
+ raise ValueError("invalid prefix or length")
+ words = [BECH32_ALPHABET.index(c) for c in address[5:]] # ValueError on a foreign character
+ if _bech32_polymod(_hrp_expand() + words) != BECH32M_CONST:
+ raise ValueError("invalid checksum")
+ acc = bits = 0
+ out = bytearray()
+ for word in words[:-6]:
+ acc = (acc << 5) | word
+ bits += 5
+ while bits >= 8:
+ bits -= 8
+ out.append((acc >> bits) & 0xFF)
+ if bits >= 5 or ((acc << (8 - bits)) & 0xFF) != 0:
+ raise ValueError("invalid padding")
+ if len(out) != 32:
+ raise ValueError("invalid payload")
+ return bytes(out)
+ except ValueError as exc:
+ raise InvalidRecipientError(f"Invalid Aleo recipient address: {address}") from exc
+
+
+def bytes32_to_aleo_address(data: bytes) -> str:
+ """Inverse of :func:`aleo_address_to_bytes32` — re-encode 32 bytes as a checksummed ``aleo1…`` address."""
+ raw = bytes(data)
+ if len(raw) != 32:
+ raise InvalidRecipientError(f"Invalid 32-byte Aleo recipient: {to_hex(raw)}")
+ acc = bits = 0
+ words: list[int] = []
+ for byte in raw:
+ acc = (acc << 8) | byte
+ bits += 8
+ while bits >= 5:
+ bits -= 5
+ words.append((acc >> bits) & 31)
+ if bits:
+ words.append((acc << (5 - bits)) & 31)
+ checksum = _bech32_polymod(_hrp_expand() + words + [0] * 6) ^ BECH32M_CONST
+ words += [(checksum >> (5 * (5 - i))) & 31 for i in range(6)]
+ return _HRP + "1" + "".join(BECH32_ALPHABET[w] for w in words)
+
+
+# ── EVM addresses ─────────────────────────────────────────────────────────────
+
+def to_checksum_address(address: str) -> str:
+ """EIP-55 checksum form of a 20-byte hex address."""
+ body = address[2:].lower()
+ digest = keccak256(body.encode()).hex()
+ return "0x" + "".join(c.upper() if int(digest[i], 16) >= 8 else c for i, c in enumerate(body))
+
+
+def is_evm_address(value: Any) -> bool:
+ """20-byte hex; mixed case must be a valid EIP-55 checksum (viem ``isAddress`` semantics)."""
+ if not isinstance(value, str) or not _EVM_ADDRESS_RE.match(value):
+ return False
+ body = value[2:]
+ if body == body.lower() or body == body.upper():
+ return True
+ return to_checksum_address(value) == value
+
+
+def evm_address_to_bytes32(address: str) -> bytes:
+ """Left-pad a checksum-validated EVM address to 32 bytes (xReserve burn recipient / Hyperlane bytes32)."""
+ if not is_evm_address(address):
+ raise InvalidRecipientError(f"Invalid Ethereum recipient address: {address}")
+ return bytes(12) + bytes.fromhex(address[2:])
+
+
+# ── Hyperlane recipient limbs and Aleo literals ───────────────────────────────
+
+def bytes32_to_u128_limbs(data: bytes) -> tuple[int, int]:
+ """Two little-endian u128 limbs over ``bytes[0:16]`` and ``bytes[16:32]``."""
+ raw = bytes(data)
+ if len(raw) != 32:
+ raise InvalidRecipientError(f"Hyperlane recipient limbs need exactly 32 bytes, got {len(raw)}")
+ return int.from_bytes(raw[:16], "little"), int.from_bytes(raw[16:], "little")
+
+
+def evm_address_to_hyperlane_recipient(address: str) -> tuple[int, int]:
+ if not is_evm_address(address):
+ raise InvalidRecipientError(f"Invalid Ethereum Hyperlane recipient: {address}")
+ return bytes32_to_u128_limbs(evm_address_to_bytes32(address))
+
+
+def solana_address_to_hyperlane_recipient(address: str) -> tuple[int, int]:
+ try:
+ raw = b58decode(address)
+ except ValueError as exc:
+ raise InvalidRecipientError(f"Invalid Solana Hyperlane recipient: {address}") from exc
+ if len(raw) != 32:
+ raise InvalidRecipientError(f"Invalid Solana Hyperlane recipient: {address}")
+ return bytes32_to_u128_limbs(raw)
+
+
+def u128_pair_literal(limbs: tuple[int, int]) -> str:
+ """``[{lo}u128, {hi}u128]`` — the Warp Route recipient input (space after the comma)."""
+ lo, hi = limbs
+ return f"[{lo}u128, {hi}u128]"
+
+
+def u8_array_literal(data: bytes) -> str:
+ """``[0u8,255u8]`` — veil ``xReserveHexToAleoBytes``: NO space after the comma."""
+ return "[" + ",".join(f"{b}u8" for b in bytes(data)) + "]"
+
+
+def hyperlane_delivery_key(message_id: bytes) -> str:
+ """``hyp_mailbox.aleo/deliveries`` key: ``{ id: [{lo}u128, {hi}u128] }``."""
+ lo, hi = bytes32_to_u128_limbs(hex_to_bytes(message_id, 32))
+ return f"{{ id: [{lo}u128, {hi}u128] }}"
+
+
+# ── Circle xReserve ───────────────────────────────────────────────────────────
+
+def _uint_be(value: int, width: int) -> bytes:
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value >= 1 << (8 * width):
+ raise InvalidAmountError(f"Unsigned value does not fit in {width} bytes: {value!r}")
+ return value.to_bytes(width, "big")
+
+
+def xreserve_deposit_nonce(source_domain: int, tx_hash: "bytes | str", log_index: int) -> bytes:
+ """Circle's deposit nonce: ``keccak(abi.encode(uint32 domain) ‖ txHash ‖ abi.encode(uint256 logIndex))``."""
+ _uint_be(source_domain, 4) # bound to uint32, consistent with the payload's remote_domain check below
+ return keccak256(_uint_be(source_domain, 32) + hex_to_bytes(tx_hash, 32) + _uint_be(log_index, 32))
+
+
+def xreserve_deposit_payload(*, amount: int, remote_domain: int, remote_token: bytes, remote_recipient: bytes,
+ local_token: str, depositor: str, max_fee: int, nonce: bytes,
+ hook_data: bytes) -> bytes:
+ """The canonical 305-byte xReserve v2 deposit payload Circle signs.
+
+ header[0..8) amount[8..40) domain[40..44) remoteToken[44..76) recipient[76..108)
+ localToken[108..140) depositor[140..172) maxFee[172..204) nonce[204..236) hookLen[236..240) hook[240..305)
+ """
+ for name, value, width in (("remote_token", remote_token, 32), ("remote_recipient", remote_recipient, 32),
+ ("nonce", nonce, 32), ("hook_data", hook_data, HOOK_DATA_BYTES)):
+ if len(bytes(value)) != width:
+ raise InvalidRecipientError(f"{name} must contain {width} bytes")
+ out = bytearray(XRESERVE_PAYLOAD_BYTES)
+ out[0:8] = _PAYLOAD_HEADER
+ out[8:40] = _uint_be(amount, 32)
+ out[40:44] = _uint_be(remote_domain, 4)
+ out[44:76] = bytes(remote_token)
+ out[76:108] = bytes(remote_recipient)
+ out[108:140] = evm_address_to_bytes32(local_token)
+ out[140:172] = evm_address_to_bytes32(depositor)
+ out[172:204] = _uint_be(max_fee, 32)
+ out[204:236] = bytes(nonce)
+ out[236:240] = _HOOK_LENGTH_FIELD
+ out[240:305] = bytes(hook_data)
+ return bytes(out)
+
+
+def xreserve_message_hash(payload: bytes) -> bytes:
+ """Circle's attestation lookup key: ``keccak256(payload)``."""
+ return keccak256(bytes(payload))
+
+
+def xreserve_nonce_from_payload(payload: bytes) -> bytes:
+ """Deposit nonce (bytes 204..236) from a canonical payload; validates header, width and hook length."""
+ raw = bytes(payload)
+ if len(raw) != XRESERVE_PAYLOAD_BYTES or raw[0:8] != _PAYLOAD_HEADER or raw[236:240] != _HOOK_LENGTH_FIELD:
+ raise AttestationError("xReserve payload has an invalid deposit layout")
+ return raw[204:236]
+
+
+def xreserve_hook_data(mode: str, recipient: str, network: str, secret_nonce: str = "0scalar") -> bytes:
+ """65-byte xReserve hook: byte 0 selects the mint transition (0 public, 1 record, 2 private);
+ private mode carries ``BHP256.commit(bits(recipient), secret_nonce)`` in bytes 1..33."""
+ if mode not in MINT_MODES:
+ raise ConfigurationError(f"Unsupported mint mode {mode!r}; expected one of {MINT_MODES}")
+ aleo_address_to_bytes32(recipient) # InvalidRecipientError before any FFI work
+ out = bytearray(HOOK_DATA_BYTES)
+ out[0] = MINT_MODES.index(mode)
+ if mode == "private":
+ validate_scalar(secret_nonce)
+ net = network_module(network)
+ bits = net.Plaintext.from_string(recipient).to_bits_le()
+ commitment = field_bytes_le(net.BHP256().commit(bits, net.Scalar.from_string(secret_nonce)))
+ if len(commitment) != 32:
+ raise ConfigurationError("Private mint commitment must contain 32 bytes")
+ out[1:33] = commitment
+ return bytes(out)
+
+
+def aleo_program_address(program_id: str, network: str) -> str:
+ """The ``aleo1…`` account owned by a deployed program (private xReserve deposits target the wrapper's)."""
+ return str(network_module(network).Address.from_program_id(program_id))
+
+
+__all__ = [
+ "BECH32_ALPHABET", "HOOK_DATA_BYTES", "MINT_MODES", "NETWORKS", "XRESERVE_PAYLOAD_BYTES",
+ "aleo_address_to_bytes32", "aleo_program_address", "bytes32_to_aleo_address", "bytes32_to_u128_limbs",
+ "evm_address_to_bytes32", "evm_address_to_hyperlane_recipient", "field_bytes_le", "hex_to_bytes",
+ "hyperlane_delivery_key", "is_evm_address", "network_module", "solana_address_to_hyperlane_recipient",
+ "to_checksum_address", "to_hex", "u128_pair_literal", "u8_array_literal", "validate_scalar",
+ "xreserve_deposit_nonce", "xreserve_deposit_payload", "xreserve_hook_data", "xreserve_message_hash",
+ "xreserve_nonce_from_payload",
+]
diff --git a/bridge-sdk/python/aleo_bridge/errors.py b/bridge-sdk/python/aleo_bridge/errors.py
new file mode 100644
index 0000000..6252db0
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/errors.py
@@ -0,0 +1,93 @@
+"""Error taxonomy — every failure raised by aleo_bridge is a BridgeError whose message states the remedy."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING: # pragma: no cover
+ from .types import Progress, Status
+
+
+class BridgeError(Exception):
+ """Base class for every error raised by aleo_bridge."""
+
+
+class ConfigurationError(BridgeError):
+ """The client, environment, or registry is configured inconsistently."""
+
+
+class MissingExtraError(BridgeError):
+ """An optional dependency group is required for this feature."""
+
+ def __init__(self, extra: str, feature: str) -> None:
+ self.extra = extra
+ self.feature = feature
+ super().__init__(f"{feature} requires the '{extra}' extra: pip install 'aleo-bridge-sdk[{extra}]'")
+
+
+class RouteNotFoundError(BridgeError):
+ """No registry entry matches the lookup (route, asset, or chain)."""
+
+
+class AmbiguousRouteError(BridgeError):
+ """More than one route matches; pass protocol= to disambiguate."""
+
+
+class RouteUnavailableError(BridgeError):
+ """The route exists but is metadata-required, disabled, or carries placeholder configuration."""
+
+
+class RegistryVersionMismatchError(BridgeError):
+ """A plan or checkpoint was prepared against a different registry version."""
+
+
+class UnsupportedRouteError(BridgeError):
+ """No implementation exists for this (protocol, chain family) combination or asset capability."""
+
+
+class InvalidAmountError(BridgeError):
+ """The amount is malformed, too precise, zero, or below the route minimum."""
+
+
+class InvalidRecipientError(BridgeError):
+ """The recipient does not match the destination chain's address format."""
+
+
+class InsufficientBalanceError(BridgeError):
+ """The account cannot cover the amount (public balance or no covering record)."""
+
+
+class ChainMismatchError(BridgeError):
+ """The connected EVM chain id / Solana genesis does not match the route."""
+
+
+class NotResumableError(BridgeError):
+ """The progress is not in a resumable or completable state."""
+
+
+class CheckpointInvalidError(BridgeError):
+ """A checkpoint fails the version-1 allowlist or does not match its plan."""
+
+
+class AttestationError(BridgeError):
+ """Circle's response is malformed, does not hash to the requested message, or the secret does not open the commitment."""
+
+
+class DeliveryUnknownError(BridgeError):
+ """Delivery could not be determined from the destination chain."""
+
+
+class PollingTimeoutError(BridgeError):
+ """``wait`` gave up; carries the last observed status and progress (timeout is not failure)."""
+
+ def __init__(self, message: str, *, status: "Status | str", progress: "Progress | None" = None) -> None:
+ self.status: Any = status
+ self.progress = progress
+ super().__init__(message)
+
+
+__all__ = [
+ "BridgeError", "ConfigurationError", "MissingExtraError", "RouteNotFoundError", "AmbiguousRouteError",
+ "RouteUnavailableError", "RegistryVersionMismatchError", "UnsupportedRouteError", "InvalidAmountError",
+ "InvalidRecipientError", "InsufficientBalanceError", "ChainMismatchError", "NotResumableError",
+ "CheckpointInvalidError", "AttestationError", "DeliveryUnknownError", "PollingTimeoutError",
+]
diff --git a/bridge-sdk/python/aleo_bridge/eth.py b/bridge-sdk/python/aleo_bridge/eth.py
new file mode 100644
index 0000000..a5df096
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/eth.py
@@ -0,0 +1,1440 @@
+"""Ethereum connection and the ``bridge.eth`` module (Hyperlane + xReserve, Ethereum origin).
+
+``web3`` and ``eth_account`` are imported lazily so ``import aleo_bridge`` works
+without the ``evm`` extra; the first call that needs them raises
+``MissingExtraError("evm", ...)``.
+"""
+from __future__ import annotations
+
+import os
+import re
+from dataclasses import dataclass, fields
+from typing import Any, Mapping
+
+from . import encoding
+from ._calls import EvmCall, EvmOutcome, EvmStep
+from ._evm_abi import ERC20_ABI, EVM_CHAIN_BY_ENVIRONMENT, MAILBOX_ABI, WARP_ROUTE_ABI, XRESERVE_ABI, ZERO_ADDRESS
+from ._plan import build_plan as _plan_for # kept as a module-level name: existing callers import eth._plan_for
+from .checkpoint import Checkpoint
+from .errors import (AmbiguousRouteError, BridgeError, ChainMismatchError, CheckpointInvalidError, ConfigurationError,
+ InsufficientBalanceError, InvalidAmountError, InvalidRecipientError, MissingExtraError,
+ RegistryVersionMismatchError, RouteNotFoundError, RouteUnavailableError, UnsupportedRouteError)
+from .registry import Asset, Chain, Registry, Route
+from .types import (ChainStatus, DepositReceipt, DispatchReceipt, EvmHyperlaneQuote, EvmXReserveQuote, Fee, Plan,
+ Receipt, Status)
+from .units import format_decimal_amount, parse_decimal_amount, resolve_amount
+
+
+def _web3():
+ try:
+ import web3
+ except ImportError as exc: # pragma: no cover - exercised by test_import_without_web3
+ raise MissingExtraError("evm", "Ethereum connections") from exc
+ return web3
+
+
+_HASH_RE = re.compile(r"^0x[0-9a-fA-F]{64}$")
+
+LOG_SCAN_CHUNK_BLOCKS = 5_000
+"""Default block span per ``eth_getLogs`` request during recovery scans.
+
+Public RPC endpoints cap the range (and the result size) of a single ``eth_getLogs``; an
+unbounded ``{"fromBlock": n}`` filter is refused outright by most of them once ``n`` is far
+enough behind the head. Recovery therefore walks the range in chunks of this many blocks.
+"""
+
+
+def _provider_errors() -> tuple[type[BaseException], ...]:
+ """Everything a JSON-RPC provider can throw for one ``eth_getLogs``: web3's own errors, the
+ ``ValueError`` older/raw providers raise for a JSON-RPC error response, and transport errors."""
+ from web3.exceptions import Web3Exception
+
+ errors: list[type[BaseException]] = [Web3Exception, ValueError]
+ try:
+ import requests
+ except ImportError: # pragma: no cover - requests ships with web3's HTTP provider
+ pass
+ else:
+ errors.append(requests.RequestException)
+ return tuple(errors)
+
+
+def _eth_account():
+ try:
+ from eth_account import Account
+ except ImportError as exc: # pragma: no cover
+ raise MissingExtraError("evm", "Ethereum signing") from exc
+ return Account
+
+
+class Ethereum:
+ """Transport + optional signer for Ethereum-origin bridge actions.
+
+ Three interchangeable forms::
+
+ Ethereum(rpc_url, private_key=key) # SDK builds Web3(HTTPProvider(rpc_url))
+ Ethereum(w3=my_w3, signer=local_account) # caller's Web3, caller's eth_account signer
+ Ethereum(w3=my_w3) # signs via w3.eth.default_account + caller middleware,
+ # else read-only
+
+ Sending: with a ``LocalAccount`` the SDK fills nonce/gas/fee fields, signs, and
+ ``send_raw_transaction``s; in default-account mode it calls
+ ``w3.eth.send_transaction`` so the caller's middleware signs. Receipts are
+ polled on the same ``Web3``.
+ """
+
+ def __init__(self, rpc_url: str | None = None, *, w3: Any = None, signer: Any = None,
+ private_key: str | None = None) -> None:
+ if (rpc_url is None) == (w3 is None):
+ raise ConfigurationError("Pass exactly one of rpc_url or w3 to Ethereum(...)")
+ if signer is not None and private_key is not None:
+ raise ConfigurationError("Pass at most one of signer or private_key to Ethereum(...)")
+ if w3 is None:
+ web3 = _web3()
+ w3 = web3.Web3(web3.HTTPProvider(rpc_url))
+ if private_key is not None:
+ signer = _eth_account().from_key(private_key)
+ self._w3 = w3
+ self._signer = signer
+ self._chain_id: int | None = None
+
+ @classmethod
+ def from_env(cls, env: Mapping[str, str] | None = None) -> "Ethereum | None":
+ """``EVM_PRIVATE_KEY`` + ``ETHEREUM_RPC_URL`` (both or neither) → signing connection; neither → None.
+
+ Aliases (the user's live shell / veil config export these names instead):
+ ``BRIDGE_EVM_PRIVATE_KEY`` for the key, ``BRIDGE_LIVE_ETHEREUM_RPC_URL`` for the RPC url.
+ The primary variable wins when both a primary and its alias are set; the both-or-neither
+ rule applies to whichever pair resolves (primary, falling back to alias, per variable).
+ """
+ env = os.environ if env is None else env
+ key = env.get("EVM_PRIVATE_KEY") or env.get("BRIDGE_EVM_PRIVATE_KEY")
+ url = env.get("ETHEREUM_RPC_URL") or env.get("BRIDGE_LIVE_ETHEREUM_RPC_URL")
+ if bool(key) != bool(url):
+ raise ConfigurationError(
+ "Set both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL or neither "
+ "(aliases: BRIDGE_EVM_PRIVATE_KEY, BRIDGE_LIVE_ETHEREUM_RPC_URL)")
+ if not key:
+ return None
+ return cls(url, private_key=key)
+
+ @property
+ def w3(self) -> Any:
+ return self._w3
+
+ @property
+ def address(self) -> str | None:
+ """Checksummed sender address: signer → ``w3.eth.default_account`` → ``None``."""
+ if self._signer is not None:
+ return self._signer.address
+ default = getattr(self._w3.eth, "default_account", None)
+ if isinstance(default, str) and default:
+ return _web3().Web3.to_checksum_address(default)
+ return None
+
+ @property
+ def can_sign(self) -> bool:
+ return self.address is not None
+
+ @property
+ def chain_id(self) -> int:
+ """``eth_chainId``, read once and cached."""
+ if self._chain_id is None:
+ self._chain_id = int(self._w3.eth.chain_id)
+ return self._chain_id
+
+ def require_address(self) -> str:
+ address = self.address
+ if address is None:
+ raise ConfigurationError(
+ "This Ethereum connection is read-only: pass private_key= or signer= to Ethereum(...), "
+ "or set w3.eth.default_account with signing middleware")
+ return address
+
+ def send_transaction(self, tx: dict) -> str:
+ """Broadcast one transaction and return its ``0x`` hash.
+
+ Fills ``from``/``chainId``/``value`` when missing. Local signer: also fills
+ ``nonce``/``gas``/fee fields, signs, ``send_raw_transaction``. Default-account
+ mode: ``send_transaction`` (the caller's middleware signs and fills gas).
+ Read-only: ``ConfigurationError``.
+ """
+ sender = self.require_address()
+ Web3 = _web3().Web3
+ tx = dict(tx)
+ tx.setdefault("from", sender)
+ if Web3.to_checksum_address(tx["from"]) != sender:
+ raise ConfigurationError(f"Transaction sender {tx['from']} does not match the configured account {sender}")
+ tx.setdefault("chainId", self.chain_id)
+ tx.setdefault("value", 0)
+ if self._signer is None:
+ # Default-account mode: the caller's middleware signs, so the hash only exists once the node
+ # answers. There is nothing to capture beforehand and nothing to compare the answer against,
+ # so the ambiguous-send protections below (lost response, echo mismatch, EvmCall's single-use
+ # guard) cannot apply here — a failed send leaves the caller unable to tell whether the
+ # transaction is in the mempool. Prefer a local signer (private_key=/signer=) for anything
+ # that moves funds.
+ return Web3.to_hex(self._w3.eth.send_transaction(tx))
+ tx.setdefault("nonce", self._w3.eth.get_transaction_count(sender, "pending"))
+ if "gas" not in tx:
+ estimate_fields = {k: v for k, v in tx.items() if k in ("from", "to", "data", "value")}
+ tx["gas"] = int(self._w3.eth.estimate_gas(estimate_fields)) * 12 // 10
+ if "gasPrice" not in tx and "maxFeePerGas" not in tx:
+ base_fee = self._w3.eth.get_block("latest").get("baseFeePerGas")
+ if base_fee is None:
+ tx["gasPrice"] = int(self._w3.eth.gas_price)
+ else:
+ tip = int(self._w3.eth.max_priority_fee)
+ tx["maxPriorityFeePerGas"] = tip
+ tx["maxFeePerGas"] = int(base_fee) * 2 + tip
+ signed = self._signer.sign_transaction(tx)
+ # The hash is fixed by the signature, so it exists before the broadcast. Capture it first: if the
+ # RPC answer is lost the node may still have accepted the bytes, and a caller who never learns the
+ # hash cannot tell a failed send from a landed one (and would resend, risking a double spend).
+ local_hash = Web3.to_hex(signed.hash)
+ try:
+ echoed = Web3.to_hex(self._w3.eth.send_raw_transaction(signed.raw_transaction))
+ except Exception as exc: # noqa: BLE001 — any transport/JSON-RPC failure loses the response, not the send
+ error = BridgeError(
+ f"Ethereum transaction {local_hash} may have been broadcast; the RPC response was lost: {exc}"
+ " — check bridge.eth.source_status / the explorer before retrying")
+ # EvmCall reads this to arm its single-use guard: an ambiguous send must not be retried.
+ error.broadcast_id = local_hash # type: ignore[attr-defined]
+ raise error from exc
+ if echoed.lower() != local_hash.lower():
+ error = BridgeError(
+ f"Ethereum RPC echoed transaction hash {echoed} for a transaction signed as {local_hash}; "
+ "refusing to checkpoint or follow the wrong hash — check bridge.eth.source_status / the "
+ f"explorer for {local_hash} before retrying")
+ # The node ANSWERED, so it took the bytes: they may sit in its mempool under local_hash even
+ # though it echoed something else. That is the same ambiguity as a lost response, so arm
+ # EvmCall's single-use guard here too rather than letting a retry sign a second transfer.
+ error.broadcast_id = local_hash # type: ignore[attr-defined]
+ raise error
+ return local_hash
+
+ def wait_for_receipt(self, tx_hash: str, *, timeout_seconds: float, poll_seconds: float) -> dict | None:
+ """Poll ``wait_for_transaction_receipt``; ``None`` on timeout (a timeout is not a failure)."""
+ from web3.exceptions import TimeExhausted
+
+ try:
+ return self._w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout_seconds, poll_latency=poll_seconds)
+ except TimeExhausted:
+ return None
+
+ def get_receipt(self, tx_hash: str) -> dict | None:
+ """One ``eth_getTransactionReceipt`` read; ``None`` while the transaction is unmined or unknown."""
+ from web3.exceptions import TransactionNotFound
+
+ try:
+ return self._w3.eth.get_transaction_receipt(tx_hash)
+ except TransactionNotFound:
+ return None
+
+
+_REGISTRY_COMMIT_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
+
+
+@dataclass(frozen=True)
+class _HyperlaneRouteMetadata:
+ """Validated Hyperlane route metadata (mirrors veil ``protocols/hyperlane/evm.ts`` ``routeMetadata``).
+
+ Every address/domain that reaches a contract call is checked here first, so a corrupted or
+ malformed registry entry fails with ``ConfigurationError`` before any RPC read.
+ """
+
+ router: str
+ router_type: str # "native" | "collateral"
+ token: str | None # collateral ERC-20; None on native
+ mailbox: str
+ interchain_gas_paymaster: str
+ interchain_security_module: str
+ source_chain_id: int
+ destination_domain: int
+ destination_router: str
+ registry_commit: str
+ requires_approval_reset: bool
+
+
+@dataclass(frozen=True)
+class _HyperlaneQuote:
+ """Router-level facts behind an ``EvmHyperlaneQuote`` (addresses never leave the module)."""
+
+ router: str
+ router_type: str # "native" | "collateral"
+ token: str | None # collateral ERC-20
+ destination_domain: int
+ recipient_bytes32: bytes
+ amount_atomic: int
+ native_value_atomic: int
+ native_fee_atomic: int
+ token_amount_atomic: int # 0 on native routes
+ allowance_atomic: int | None
+ requires_approval_reset: bool
+
+
+_DIGIT_STRING_RE = re.compile(r"^[0-9]+$")
+
+
+@dataclass(frozen=True)
+class _XReserveRouteMetadata:
+ """Validated xReserve route metadata (mirrors veil ``protocols/xreserve/evmToAleo.ts`` ``routeMetadata``).
+
+ Every address, domain, program name and fee that reaches a contract call or an Aleo encoder is
+ checked here first, so a corrupted or malformed registry entry fails with ``ConfigurationError``
+ before any RPC read.
+ """
+
+ xreserve_contract: str
+ source_chain_id: int
+ source_domain: int
+ remote_domain: int
+ remote_token_bytes32: bytes
+ minimum_amount_atomic: int
+ withdrawal_fee_atomic: int
+ max_fee_atomic: int
+ bridge_program: str
+ wrapper_program: str
+ remote_token: str
+ attestation_base_url: str
+
+
+@dataclass(frozen=True)
+class _XReserveQuote:
+ """Contract-level facts behind an ``EvmXReserveQuote``; also rebuilt from receipts during status/recovery."""
+
+ xreserve_contract: str
+ token: str
+ source_chain_id: int
+ source_domain: int
+ remote_domain: int
+ remote_token_bytes32: bytes
+ remote_recipient_bytes32: bytes
+ amount_atomic: int
+ max_fee_atomic: int
+ hook_data: bytes
+ balance_atomic: int
+ allowance_atomic: int
+ bridge_program: str
+ wrapper_program: str
+
+
+class EthModule:
+ """``bridge.eth`` — Ethereum-origin Hyperlane and xReserve actions (reads return values, writes return ``EvmCall``)."""
+
+ def __init__(self, bridge: Any, conn: Ethereum, *, log_scan_chunk_blocks: int = LOG_SCAN_CHUNK_BLOCKS) -> None:
+ self.bridge = bridge
+ self.conn = conn
+ self.registry: Registry = bridge.registry
+ self.network: str = bridge.network # "mainnet" | "testnet" → aleo. for encoders
+ self.chain: Chain = self.registry.chain(EVM_CHAIN_BY_ENVIRONMENT[bridge.environment])
+ self.log_scan_chunk_blocks = log_scan_chunk_blocks # recovery eth_getLogs span; lower it for strict RPCs
+
+ @property
+ def log_scan_chunk_blocks(self) -> int:
+ """Blocks per ``eth_getLogs`` request during recovery scans; lower it for strict RPCs.
+
+ Validated on every assignment, not just in the constructor: ``_scan_logs`` advances its
+ cursor by this many blocks per pass, so a zero or negative chunk would loop forever
+ against a live chain rather than fail.
+ """
+ return self._log_scan_chunk_blocks
+
+ @log_scan_chunk_blocks.setter
+ def log_scan_chunk_blocks(self, value: Any) -> None:
+ if int(value) < 1:
+ raise ConfigurationError("log_scan_chunk_blocks must be at least 1")
+ self._log_scan_chunk_blocks = int(value)
+
+ # -- resolution ---------------------------------------------------------------------------
+
+ def _asset(self, ref: Any) -> Asset:
+ """Accept an ``Asset``, ``"chain/key"``, ``(chain, key)``, or a bare key/symbol on this chain."""
+ if isinstance(ref, Asset):
+ return ref
+ if isinstance(ref, tuple) or (isinstance(ref, str) and "/" in ref):
+ return self.registry.asset(ref)
+ matches = [a for a in self.registry.assets(chain=self.chain.id)
+ if a.key.lower() == str(ref).lower() or a.symbol.lower() == str(ref).lower()]
+ if len(matches) != 1:
+ raise RouteNotFoundError(f"No unique asset {ref!r} on {self.chain.id}; use 'chain/key'")
+ return matches[0]
+
+ def _hyperlane_route(self, asset: Asset) -> Route:
+ if asset.chain_id != self.chain.id:
+ raise RouteNotFoundError(f"{asset.id} is not on {self.chain.id}; bridge.eth drives {self.chain.id} only")
+ candidates = [r for r in self.registry.routes(protocol="hyperlane", include_unavailable=True,
+ environment=self.bridge.environment)
+ if r.source_asset_id == asset.id]
+ if not candidates:
+ if any(r.source_asset_id == asset.id for r in self.registry.routes(include_unavailable=True,
+ environment=self.bridge.environment)):
+ raise BridgeError(f"{asset.id} is not a Hyperlane route source; use deposit_usdc for xReserve")
+ raise RouteNotFoundError(f"No Hyperlane route from {asset.id}")
+ active = [r for r in candidates if r.availability == "active"]
+ if not active:
+ raise RouteUnavailableError(f"Hyperlane route is not executable ({candidates[0].availability}): {candidates[0].id}")
+ if len(active) > 1:
+ raise AmbiguousRouteError(f"{len(active)} active Hyperlane routes from {asset.id}; pass route=")
+ return active[0]
+
+ def _xreserve_route(self) -> Route:
+ routes = [r for r in self.registry.routes(protocol="xreserve", environment=self.bridge.environment)
+ if self.registry.asset(r.source_asset_id).chain_id == self.chain.id]
+ if len(routes) != 1:
+ raise RouteNotFoundError(f"Expected exactly one xReserve deposit route from {self.chain.id}, found {len(routes)}")
+ if routes[0].availability != "active":
+ raise RouteUnavailableError(f"xReserve route is not executable: {routes[0].id}")
+ return routes[0]
+
+ def _route_for_plan(self, plan: Plan) -> Route:
+ """Re-resolve the route from the live registry (invariant 1); never trust plan-carried addresses."""
+ if plan.registry_version != self.registry.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {self.registry.version}")
+ route = self.registry.route(plan.route_id)
+ if route.source_asset_id != plan.source_asset_id or route.destination_asset_id != plan.destination_asset_id:
+ raise BridgeError(f"Plan assets do not match configured route {route.id}")
+ if route.availability != "active":
+ raise RouteUnavailableError(f"Route is not executable: {route.id}")
+ return route
+
+ def _plan_route(self, plan: Plan, protocol: str) -> Route:
+ """Re-resolve a caller-supplied ``Plan``'s route by id (never trust plan-carried addresses)."""
+ if plan.registry_version != self.registry.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {self.registry.version}")
+ try:
+ route = self.registry.route(plan.route_id)
+ except RouteNotFoundError as exc:
+ raise RouteUnavailableError(f"Plan route {plan.route_id} is not in registry {self.registry.version}") from exc
+ if route.protocol != protocol:
+ raise RouteUnavailableError(f"{route.id} is a {route.protocol} route, not a {protocol} one")
+ if route.availability != "active":
+ raise RouteUnavailableError(f"Route is not executable ({route.availability}): {route.id}")
+ return route
+
+ def _plan_sender(self, plan: Plan, *, require_signer: bool) -> str:
+ """``plan.sender`` as a checksummed EVM address, bound to the connected account when there is one."""
+ Web3 = _web3().Web3
+ sender = plan.sender
+ if not isinstance(sender, str) or not Web3.is_address(sender) or Web3.to_checksum_address(sender) != sender:
+ raise BridgeError(f"Plan sender must be a checksummed EVM address; got {sender!r}")
+ connected = self.conn.require_address() if require_signer else self.conn.address
+ if connected is not None and sender != connected: # the rule EvmCall.send() applies
+ raise ConfigurationError(f"Prepared sender {sender} does not match connected account {connected}")
+ return sender
+
+ def _assert_plan_matches(self, plan: Plan, route: Route, *, sender: str, recipient: str, amount_atomic: int,
+ mint_mode: str) -> None:
+ """The plan must be exactly what this module would have prepared for the same transfer."""
+ rebuilt = _plan_for(self.registry, route, amount_atomic=amount_atomic, recipient=recipient, sender=sender,
+ mint_mode=mint_mode)
+ for field in fields(Plan):
+ mine, theirs = getattr(rebuilt, field.name), getattr(plan, field.name)
+ if mine != theirs:
+ raise BridgeError(f"plan does not match the requested transfer: {field.name} is {theirs!r} "
+ f"but this transfer prepares {mine!r}")
+
+ def _from_plan(self, plan: Plan, protocol: str, *, recipient: str | None, amount: Any, amount_atomic: int | None,
+ mint_mode: str | None, require_signer: bool) -> tuple[Route, str, str, int, str]:
+ """Validate ``plan=`` and return ``(route, sender, recipient, amount_atomic, mint_mode)``.
+
+ Explicit ``recipient``/``amount``/``mint_mode`` arguments override the plan's own values and are
+ then caught by the field-by-field equality check, so a plan can never silently disagree with
+ the call that carries it.
+ """
+ route = self._plan_route(plan, protocol)
+ sender = self._plan_sender(plan, require_signer=require_signer)
+ recipient = plan.recipient if recipient is None else recipient
+ mint_mode = plan.mint_mode if mint_mode is None else mint_mode
+ if amount is None and amount_atomic is None:
+ amount_atomic = plan.amount_atomic
+ atomic = self._amount_atomic(route, amount, amount_atomic)
+ self._assert_plan_matches(plan, route, sender=sender, recipient=recipient, amount_atomic=atomic,
+ mint_mode=mint_mode)
+ return route, sender, recipient, atomic, mint_mode
+
+ def assert_chain(self, route: Route) -> None:
+ expected = int(route.metadata["sourceChainId"])
+ actual = self.conn.chain_id
+ if actual != expected:
+ raise ChainMismatchError(f"EVM connection is on chain {actual}; expected {expected} for {route.id}")
+
+ def _recipient_bytes32(self, route: Route, recipient: str) -> bytes:
+ destination = self.registry.asset(route.destination_asset_id)
+ if destination.address_regex and not re.fullmatch(destination.address_regex, recipient):
+ raise InvalidRecipientError(f"Recipient does not match the {destination.chain_id} address format: {recipient}")
+ return encoding.aleo_address_to_bytes32(recipient)
+
+ def _amount_atomic(self, route: Route, amount: Any, amount_atomic: int | None) -> int:
+ source = self.registry.asset(route.source_asset_id)
+ destination = self.registry.asset(route.destination_asset_id)
+ atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=source.decimals)
+ if atomic <= 0:
+ raise InvalidAmountError("Amount must be positive")
+ parse_decimal_amount(format_decimal_amount(atomic, source.decimals), destination.decimals) # precision on both sides
+ return atomic
+
+ def _owner(self, sender: str | None) -> str | None:
+ if sender is None:
+ return self.conn.address
+ return _web3().Web3.to_checksum_address(sender)
+
+ # -- contracts ----------------------------------------------------------------------------
+
+ def _contract(self, address: str, abi: list) -> Any:
+ return self.conn.w3.eth.contract(address=_web3().Web3.to_checksum_address(address), abi=abi)
+
+ def _erc20(self, address: str) -> Any:
+ return self._contract(address, ERC20_ABI)
+
+ def _native_fee(self, amount_wei: int) -> Fee:
+ native = [a for a in self.registry.assets(chain=self.chain.id) if a.kind == "native"]
+ asset_id = native[0].id if native else f"{self.chain.id}/{self.chain.native_symbol.lower()}"
+ decimals = native[0].decimals if native else 18 # only an unlisted chain falls back to the EVM default
+ return Fee(kind="network", chain_id=self.chain.id, asset_id=asset_id,
+ amount=format_decimal_amount(amount_wei, decimals), estimated=True)
+
+ # -- Hyperlane quote ----------------------------------------------------------------------
+
+ def _metadata_address(self, meta: Mapping[str, Any], key: str, route_id: str) -> str:
+ """Checksum a metadata address field; any malformed value is a ``ConfigurationError``, never
+ a raw ``ValueError``/``KeyError`` (a missing key reads as ``None`` via ``.get``, which also fails here)."""
+ value = meta.get(key)
+ try:
+ return _web3().Web3.to_checksum_address(str(value))
+ except (ValueError, TypeError) as exc:
+ raise ConfigurationError(f"Hyperlane route metadata {key!r} is not a valid address ({route_id}): {value!r}") from exc
+
+ def _hyperlane_metadata(self, route: Route) -> _HyperlaneRouteMetadata:
+ """Brief §3.1 route metadata validator (mirrors veil ``protocols/hyperlane/evm.ts`` ``routeMetadata``).
+
+ Called by every path that is about to touch a Hyperlane contract (``_quote_hyperlane``, and
+ transitively ``transfer_remote``'s step builder through the ``_HyperlaneQuote`` it returns) so
+ no unvalidated address or domain from the registry ever reaches an RPC call.
+ """
+ if route is None or route.protocol != "hyperlane" or route.availability != "active":
+ raise RouteUnavailableError(f"Hyperlane route is not executable: {getattr(route, 'id', route)!r}")
+ meta = route.metadata
+ router = self._metadata_address(meta, "routerAddress", route.id)
+ mailbox = self._metadata_address(meta, "mailboxAddress", route.id)
+ igp = self._metadata_address(meta, "interchainGasPaymaster", route.id)
+ ism = self._metadata_address(meta, "interchainSecurityModule", route.id)
+ source_chain_id = meta.get("sourceChainId")
+ if isinstance(source_chain_id, bool) or not isinstance(source_chain_id, int) or source_chain_id <= 0:
+ raise ConfigurationError(
+ f"Hyperlane route metadata sourceChainId must be a positive int ({route.id}): {source_chain_id!r}")
+ destination_domain = meta.get("destinationDomain")
+ if isinstance(destination_domain, bool) or not isinstance(destination_domain, int) \
+ or not (0 <= destination_domain <= 2**32 - 1):
+ raise ConfigurationError(
+ f"Hyperlane route metadata destinationDomain must be a uint32 ({route.id}): {destination_domain!r}")
+ router_type = meta.get("routerType")
+ if router_type not in ("native", "collateral"):
+ raise ConfigurationError(
+ f"Hyperlane route metadata routerType must be native or collateral ({route.id}): {router_type!r}")
+ token = self._metadata_address(meta, "tokenAddress", route.id) if router_type == "collateral" else None
+ destination_router = meta.get("destinationRouter")
+ if not isinstance(destination_router, str) or not destination_router.strip():
+ raise ConfigurationError(f"Hyperlane route metadata destinationRouter must be non-empty ({route.id})")
+ registry_commit = meta.get("registryCommit")
+ if not isinstance(registry_commit, str) or not _REGISTRY_COMMIT_RE.fullmatch(registry_commit):
+ raise ConfigurationError(
+ f"Hyperlane route metadata registryCommit must be 40 hex chars ({route.id}): {registry_commit!r}")
+ return _HyperlaneRouteMetadata(
+ router=router, router_type=router_type, token=token, mailbox=mailbox,
+ interchain_gas_paymaster=igp, interchain_security_module=ism, source_chain_id=source_chain_id,
+ destination_domain=destination_domain, destination_router=destination_router,
+ registry_commit=registry_commit, requires_approval_reset=meta.get("requiresApprovalReset") is True)
+
+ def _quote_hyperlane(self, route: Route, recipient_bytes32: bytes, amount_atomic: int, owner: str | None) -> _HyperlaneQuote:
+ """Brief §3.1: chain assert → metadata validation → quoteTransferRemote → native/collateral split → allowance."""
+ self.assert_chain(route)
+ meta = self._hyperlane_metadata(route)
+ Web3 = _web3().Web3
+ quotes = self._contract(meta.router, WARP_ROUTE_ABI).functions.quoteTransferRemote(
+ meta.destination_domain, recipient_bytes32, amount_atomic).call()
+ native_value = sum(int(q[1]) for q in quotes if Web3.to_checksum_address(q[0]) == ZERO_ADDRESS)
+ if meta.router_type == "native":
+ if native_value < amount_atomic:
+ raise BridgeError("Native Hyperlane quote does not cover the transfer amount")
+ return _HyperlaneQuote(meta.router, "native", None, meta.destination_domain, recipient_bytes32,
+ amount_atomic, native_value, native_value - amount_atomic, 0, None, False)
+ token_amount = sum(int(q[1]) for q in quotes if Web3.to_checksum_address(q[0]) == meta.token)
+ if token_amount < amount_atomic:
+ raise BridgeError("Collateral Hyperlane quote does not cover the transfer amount")
+ allowance = int(self._erc20(meta.token).functions.allowance(owner, meta.router).call()) if owner else None
+ return _HyperlaneQuote(meta.router, "collateral", meta.token, meta.destination_domain, recipient_bytes32,
+ amount_atomic, native_value, native_value, token_amount, allowance,
+ meta.requires_approval_reset)
+
+ def quote_transfer_remote(self, asset: Any = None, recipient: str | None = None, *, amount: Any = None,
+ amount_atomic: int | None = None, route: Route | None = None, sender: str | None = None,
+ plan: Plan | None = None) -> EvmHyperlaneQuote:
+ """Quote an Ethereum → Aleo Hyperlane transfer without signing.
+
+ Native routes (ETH): ``msg.value`` carries the asset and the relayer fee, so
+ ``native_fee_atomic = native_value_atomic - amount``. Collateral routes (WBTC, USDT):
+ ``msg.value`` is fee only and ``approval_required`` reflects the router's ERC-20
+ allowance for ``sender`` (or the connection's account); it is ``None`` when no account is known.
+
+ ``plan=`` re-quotes a plan prepared earlier: it supplies the route, sender, recipient and
+ amount, and is validated against the live registry. It is mutually exclusive with
+ ``asset=``/``route=``/``sender=``.
+ """
+ if plan is None and recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ if plan is not None:
+ if asset is not None or route is not None or sender is not None:
+ raise ValueError("Pass plan= or asset=/route=/sender=, not both")
+ route, owner, recipient, atomic, _ = self._from_plan(
+ plan, "hyperlane", recipient=recipient, amount=amount, amount_atomic=amount_atomic,
+ mint_mode=None, require_signer=False)
+ recipient32 = self._recipient_bytes32(route, recipient)
+ else:
+ if asset is None and route is None:
+ raise ValueError("quote_transfer_remote needs asset=, route= or plan=")
+ route = route or self._hyperlane_route(self._asset(asset))
+ if route.protocol != "hyperlane":
+ raise BridgeError(f"{route.id} is not a Hyperlane route; use quote_deposit_usdc for xReserve")
+ atomic = self._amount_atomic(route, amount, amount_atomic)
+ recipient32 = self._recipient_bytes32(route, recipient)
+ owner = self._owner(sender)
+ q = self._quote_hyperlane(route, recipient32, atomic, owner)
+ destination = self.registry.asset(route.destination_asset_id)
+ plan = _plan_for(self.registry, route, amount_atomic=atomic, recipient=recipient, sender=owner)
+ approval_required = None if q.allowance_atomic is None else q.allowance_atomic < q.token_amount_atomic
+ return EvmHyperlaneQuote(kind="evm-hyperlane", plan=plan, fees=(self._native_fee(q.native_fee_atomic),),
+ amount_out=format_decimal_amount(atomic, destination.decimals),
+ recipient_bytes32=recipient32, native_value_atomic=q.native_value_atomic,
+ native_fee_atomic=q.native_fee_atomic, approval_required=approval_required)
+
+ # -- xReserve quote -------------------------------------------------------------------------
+
+ def _metadata_digits(self, meta: Mapping[str, Any], key: str, route_id: str) -> int:
+ """A digit-string atomic-amount metadata field as ``int``; never bool/float/junk."""
+ value = meta.get(key)
+ if not isinstance(value, str) or not _DIGIT_STRING_RE.fullmatch(value):
+ raise ConfigurationError(f"xReserve route metadata {key!r} must be a digit string ({route_id}): {value!r}")
+ return int(value)
+
+ def _metadata_aleo_program(self, meta: Mapping[str, Any], key: str, route_id: str) -> str:
+ value = meta.get(key)
+ if not isinstance(value, str) or not value.endswith(".aleo") or value == ".aleo":
+ raise ConfigurationError(f"xReserve route metadata {key!r} must be an .aleo program name ({route_id}): {value!r}")
+ return value
+
+ def _xreserve_metadata(self, route: Route) -> _XReserveRouteMetadata:
+ """Brief §3.2 route metadata validator (mirrors veil ``protocols/xreserve/evmToAleo.ts`` ``routeMetadata``).
+
+ Called by every path that is about to touch the xReserve contract or an Aleo program address
+ (``_quote_xreserve``, ``_xreserve_recipient_bytes32``, and Task 6's deposit execute) so no
+ unvalidated address, domain, fee, or program name from the registry ever reaches an RPC call.
+ """
+ if route is None or route.protocol != "xreserve" or route.availability != "active":
+ raise RouteUnavailableError(f"xReserve route is not executable: {getattr(route, 'id', route)!r}")
+ source_chain = self.registry.chain(self.registry.asset(route.source_asset_id).chain_id)
+ if source_chain.family != "evm":
+ raise ConfigurationError(f"xReserve route source chain must be an EVM chain ({route.id}): {source_chain.id!r}")
+ expected_aleo_chain = "aleo-testnet" if self.network == "testnet" else "aleo"
+ destination_chain_id = self.registry.asset(route.destination_asset_id).chain_id
+ if destination_chain_id != expected_aleo_chain:
+ raise ConfigurationError(
+ f"xReserve route destination chain must be {expected_aleo_chain!r} ({route.id}): {destination_chain_id!r}")
+ meta = route.metadata
+ xreserve_contract = self._metadata_address(meta, "xReserveContract", route.id)
+ source_chain_id = meta.get("sourceChainId")
+ if isinstance(source_chain_id, bool) or not isinstance(source_chain_id, int) or source_chain_id <= 0:
+ raise ConfigurationError(
+ f"xReserve route metadata sourceChainId must be a positive int ({route.id}): {source_chain_id!r}")
+ source_domain = meta.get("sourceDomain")
+ if isinstance(source_domain, bool) or not isinstance(source_domain, int) or source_domain < 0:
+ raise ConfigurationError(
+ f"xReserve route metadata sourceDomain must be a non-negative int ({route.id}): {source_domain!r}")
+ remote_domain = meta.get("remoteDomain")
+ if isinstance(remote_domain, bool) or not isinstance(remote_domain, int) or remote_domain < 0:
+ raise ConfigurationError(
+ f"xReserve route metadata remoteDomain must be a non-negative int ({route.id}): {remote_domain!r}")
+ remote_token_bytes32_raw = meta.get("remoteTokenBytes32")
+ if not isinstance(remote_token_bytes32_raw, str):
+ raise ConfigurationError(
+ f"xReserve route metadata remoteTokenBytes32 must be a hex string ({route.id}): {remote_token_bytes32_raw!r}")
+ hex_text = remote_token_bytes32_raw[2:] if remote_token_bytes32_raw[:2] in ("0x", "0X") else remote_token_bytes32_raw
+ try:
+ remote_token_bytes32 = bytes.fromhex(hex_text)
+ except ValueError as exc:
+ raise ConfigurationError(
+ f"xReserve route metadata remoteTokenBytes32 is not valid hex ({route.id}): {remote_token_bytes32_raw!r}") from exc
+ if len(remote_token_bytes32) != 32:
+ raise ConfigurationError(
+ f"xReserve route metadata remoteTokenBytes32 must be exactly 32 bytes ({route.id}): {remote_token_bytes32_raw!r}")
+ minimum_amount_atomic = self._metadata_digits(meta, "minimumAmountAtomic", route.id)
+ withdrawal_fee_atomic = self._metadata_digits(meta, "withdrawalFeeAtomic", route.id)
+ max_fee_atomic = self._metadata_digits(meta, "maxFeeAtomic", route.id)
+ bridge_program = self._metadata_aleo_program(meta, "bridgeProgram", route.id)
+ wrapper_program = self._metadata_aleo_program(meta, "wrapperProgram", route.id)
+ remote_token = self._metadata_aleo_program(meta, "remoteToken", route.id)
+ attestation_base_url = meta.get("attestationBaseUrl")
+ if not isinstance(attestation_base_url, str) or not attestation_base_url.startswith("https://"):
+ raise ConfigurationError(
+ f"xReserve route metadata attestationBaseUrl must start with https:// ({route.id}): {attestation_base_url!r}")
+ return _XReserveRouteMetadata(
+ xreserve_contract=xreserve_contract, source_chain_id=source_chain_id, source_domain=source_domain,
+ remote_domain=remote_domain, remote_token_bytes32=remote_token_bytes32,
+ minimum_amount_atomic=minimum_amount_atomic, withdrawal_fee_atomic=withdrawal_fee_atomic,
+ max_fee_atomic=max_fee_atomic, bridge_program=bridge_program, wrapper_program=wrapper_program,
+ remote_token=remote_token, attestation_base_url=attestation_base_url)
+
+ def _xreserve_recipient_bytes32(self, route: Route, meta: _XReserveRouteMetadata, recipient: str,
+ mint_mode: str) -> bytes:
+ """Invariant 7: private deposits are addressed to the wrapper program's account address."""
+ self._recipient_bytes32(route, recipient) # validates the intended recipient
+ if mint_mode == "private":
+ return encoding.aleo_address_to_bytes32(encoding.aleo_program_address(meta.wrapper_program, self.network))
+ return encoding.aleo_address_to_bytes32(recipient)
+
+ def _quote_xreserve(self, route: Route, recipient: str, amount_atomic: int, owner: str | None,
+ mint_mode: str, secret_nonce: str) -> _XReserveQuote:
+ """Brief §3.2 quote: chain assert → metadata validation → minimum → hook data → wire recipient →
+ balanceOf/allowance."""
+ if mint_mode not in ("public", "record", "private"):
+ raise BridgeError(f"mint_mode must be public, record or private; got {mint_mode!r}")
+ self.assert_chain(route)
+ meta = self._xreserve_metadata(route)
+ if amount_atomic < meta.minimum_amount_atomic:
+ raise InvalidAmountError(f"xReserve minimum deposit is {meta.minimum_amount_atomic} atomic units")
+ if owner is None:
+ raise ConfigurationError("xReserve quotes read the depositor's balance: pass sender= or configure a signer")
+ source = self.registry.asset(route.source_asset_id)
+ if source.locator is None or source.locator.kind != "evm-contract":
+ raise RouteUnavailableError(f"xReserve source token contract is missing: {route.id}")
+ token = _web3().Web3.to_checksum_address(source.locator.value)
+ hook_data = encoding.xreserve_hook_data(mint_mode, recipient, self.network, secret_nonce)
+ remote_recipient = self._xreserve_recipient_bytes32(route, meta, recipient, mint_mode)
+ erc20 = self._erc20(token)
+ balance = int(erc20.functions.balanceOf(owner).call())
+ allowance = int(erc20.functions.allowance(owner, meta.xreserve_contract).call())
+ if balance < amount_atomic:
+ raise InsufficientBalanceError(f"Insufficient {source.symbol} balance: {balance} < {amount_atomic} atomic units")
+ return _XReserveQuote(
+ xreserve_contract=meta.xreserve_contract, token=token, source_chain_id=meta.source_chain_id,
+ source_domain=meta.source_domain, remote_domain=meta.remote_domain,
+ remote_token_bytes32=meta.remote_token_bytes32,
+ remote_recipient_bytes32=remote_recipient, amount_atomic=amount_atomic,
+ max_fee_atomic=meta.max_fee_atomic, hook_data=hook_data,
+ balance_atomic=balance, allowance_atomic=allowance,
+ bridge_program=meta.bridge_program, wrapper_program=meta.wrapper_program)
+
+ def quote_deposit_usdc(self, recipient: str | None = None, *, amount: Any = None, amount_atomic: int | None = None,
+ mint_mode: str | None = None, secret_nonce: str = "0scalar",
+ sender: str | None = None, route: Route | None = None,
+ plan: Plan | None = None) -> EvmXReserveQuote:
+ """Quote a USDC → USDCx xReserve deposit without signing.
+
+ Checks the 2 USDC minimum, derives the 65-byte hook (``public``/``record``/``private``;
+ private commits ``recipient`` with ``secret_nonce`` via BHP256) and the wire recipient
+ (the shielded wrapper program's address for ``private``), and reads the depositor's
+ USDC balance and xReserve allowance. ``secret_nonce`` is never stored by the SDK.
+ ``mint_mode`` defaults to ``plan.mint_mode`` when a plan is given, else ``"public"``.
+
+ ``plan=`` re-quotes a plan prepared earlier: it supplies the route, sender, recipient, amount
+ and mint mode, and is validated against the live registry. It is mutually exclusive with
+ ``route=``/``sender=``.
+ """
+ if plan is None and recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ if plan is not None:
+ if route is not None or sender is not None:
+ raise ValueError("Pass plan= or route=/sender=, not both")
+ route, owner, recipient, atomic, mint_mode = self._from_plan(
+ plan, "xreserve", recipient=recipient, amount=amount, amount_atomic=amount_atomic,
+ mint_mode=mint_mode, require_signer=False)
+ else:
+ route = route or self._xreserve_route()
+ mint_mode = "public" if mint_mode is None else mint_mode
+ atomic = self._amount_atomic(route, amount, amount_atomic)
+ owner = self._owner(sender)
+ q = self._quote_xreserve(route, recipient, atomic, owner, mint_mode, secret_nonce)
+ destination = self.registry.asset(route.destination_asset_id)
+ plan = _plan_for(self.registry, route, amount_atomic=atomic, recipient=recipient, sender=owner, mint_mode=mint_mode)
+ return EvmXReserveQuote(kind="evm-xreserve", plan=plan, fees=(),
+ amount_out=format_decimal_amount(atomic, destination.decimals),
+ hook_data=q.hook_data, remote_recipient_bytes32=q.remote_recipient_bytes32,
+ balance_atomic=q.balance_atomic, allowance_atomic=q.allowance_atomic,
+ approval_required=q.allowance_atomic < atomic, max_fee_atomic=q.max_fee_atomic)
+
+ # -- Hyperlane execute --------------------------------------------------------------------
+
+ def _message_id_from_receipt(self, route: Route, receipt: Any) -> str | None:
+ """Hyperlane Mailbox ``DispatchId(bytes32 indexed messageId)`` from a confirmed receipt; ``None`` if absent."""
+ from web3.logs import DISCARD
+
+ Web3 = _web3().Web3
+ address = self._hyperlane_metadata(route).mailbox
+ mailbox = self._contract(address, MAILBOX_ABI)
+ # process_receipt decodes by topic alone: another contract's DispatchId(bytes32) would
+ # otherwise be read as this transfer's message id, so filter on the emitting address first.
+ events = [ev for ev in mailbox.events.DispatchId().process_receipt(receipt, errors=DISCARD)
+ if Web3.to_checksum_address(ev["address"]) == address]
+ if not events:
+ return None
+ return Web3.to_hex(events[0]["args"]["messageId"]) # veil messageIdFromReceipt: first match wins
+
+ @staticmethod
+ def _hyperlane_protocol_state(route: Route, *, recipient_bytes32: bytes, destination_domain: int,
+ native_value_atomic: int, amount_atomic: int, approval_tx_ids: list[str],
+ sender: str | None, message_id: str | None = None) -> dict[str, Any]:
+ state: dict[str, Any] = {
+ "routeId": route.id, "approvalTxIds": list(approval_tx_ids), "sourceSender": sender,
+ "recipientBytes32": "0x" + recipient_bytes32.hex(), "destinationDomain": destination_domain,
+ "nativeValueAtomic": str(native_value_atomic), "amountAtomic": str(amount_atomic),
+ }
+ if message_id is not None:
+ state["messageId"] = message_id
+ return state
+
+ def _hyperlane_result(self, route: Route, q: "_HyperlaneQuote", outcome: EvmOutcome) -> DispatchReceipt:
+ approvals = list(outcome.approval_tx_ids)
+ if outcome.status == "CONFIRMED":
+ message_id = self._message_id_from_receipt(route, outcome.receipt)
+ status, rid = Status.DELIVERY_PENDING, message_id or outcome.source_tx_id
+ else:
+ message_id, status = None, Status(outcome.status)
+ rid = outcome.source_tx_id or approvals[-1]
+ state = self._hyperlane_protocol_state(
+ route, recipient_bytes32=q.recipient_bytes32, destination_domain=q.destination_domain,
+ native_value_atomic=q.native_value_atomic, amount_atomic=q.amount_atomic,
+ approval_tx_ids=approvals, sender=outcome.sender, message_id=message_id)
+ receipt = Receipt(id=rid, protocol="hyperlane", status=status, source_tx_id=outcome.source_tx_id, protocol_state=state)
+ return DispatchReceipt(transaction_id=outcome.source_tx_id or approvals[-1], route_id=route.id,
+ message_id=message_id, amount_atomic=q.amount_atomic, receipt=receipt)
+
+ def transfer_remote(self, asset: Any = None, recipient: str | None = None, *, amount: Any = None,
+ amount_atomic: int | None = None, plan: Plan | None = None) -> EvmCall[DispatchReceipt]:
+ """Send ETH, WBTC or USDT to Aleo through its Hyperlane Warp Route.
+
+ Re-quotes ``quoteTransferRemote`` at send time. Collateral routes approve exactly the
+ quoted token amount only when the allowance is short (USDT: a non-zero allowance is
+ reset to 0 first). Native ETH sends amount + fee as ``msg.value``; collateral routes
+ send the fee only. Each hash is checkpointed before polling; a timeout returns a
+ pending ``DispatchReceipt``. The message id comes from the Mailbox ``DispatchId`` log.
+
+ ``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+ re-resolved by id against the live registry, the sender must be the connected account, and
+ the plan must equal what this call would have prepared itself. Mutually exclusive with ``asset=``.
+ """
+ if plan is None and recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ if plan is not None:
+ if asset is not None:
+ raise ValueError("Pass plan= or asset=, not both")
+ route, sender, recipient, atomic, _ = self._from_plan(
+ plan, "hyperlane", recipient=recipient, amount=amount, amount_atomic=amount_atomic,
+ mint_mode=None, require_signer=True)
+ else:
+ if asset is None:
+ raise ValueError("transfer_remote needs asset= or plan=")
+ route = self._hyperlane_route(self._asset(asset))
+ sender = self.conn.require_address()
+ atomic = self._amount_atomic(route, amount, amount_atomic)
+ plan = _plan_for(self.registry, route, amount_atomic=atomic, recipient=recipient, sender=sender)
+ recipient32 = self._recipient_bytes32(route, recipient)
+ latest: dict[str, _HyperlaneQuote] = {}
+
+ def steps(owner: str) -> list[EvmStep]:
+ q = self._quote_hyperlane(route, recipient32, atomic, owner) # last responsible moment
+ latest["q"] = q
+ out: list[EvmStep] = []
+ if q.router_type == "collateral" and (q.allowance_atomic or 0) < q.token_amount_atomic:
+ token = self._erc20(q.token)
+ if (q.allowance_atomic or 0) > 0 and q.requires_approval_reset:
+ out.append(EvmStep("approve", q.token, token.encode_abi("approve", args=[q.router, 0])))
+ out.append(EvmStep("approve", q.token, token.encode_abi("approve", args=[q.router, q.token_amount_atomic])))
+ warp = self._contract(q.router, WARP_ROUTE_ABI)
+ out.append(EvmStep("main", q.router,
+ warp.encode_abi("transferRemote", args=[q.destination_domain, recipient32, atomic]),
+ q.native_value_atomic))
+ return out
+
+ def finish(outcome: EvmOutcome) -> DispatchReceipt:
+ return self._hyperlane_result(route, latest["q"], outcome)
+
+ return EvmCall(self.conn, plan=plan, registry=self.registry, steps=steps, finish=finish,
+ store=self.bridge.checkpoints)
+
+ # -- xReserve execute -----------------------------------------------------------------------
+
+ @staticmethod
+ def _xreserve_protocol_state(route: Route, q: _XReserveQuote, *, approval_tx_ids: list[str], sender: str | None,
+ mint_mode: str, intended_recipient: str) -> dict[str, Any]:
+ return {
+ "routeId": route.id, "approvalTxIds": list(approval_tx_ids), "sourceSender": sender,
+ "mintMode": mint_mode, "intendedRecipient": intended_recipient,
+ "xReserveContract": q.xreserve_contract, "tokenAddress": q.token, "sourceChainId": q.source_chain_id,
+ "remoteDomain": q.remote_domain, "remoteRecipientBytes32": "0x" + q.remote_recipient_bytes32.hex(),
+ "hookData": "0x" + q.hook_data.hex(), "amountAtomic": str(q.amount_atomic), "maxFeeAtomic": str(q.max_fee_atomic),
+ }
+
+ def _confirmed_deposit_receipt(self, route: Route, q: _XReserveQuote, *, owner: str, approval_tx_ids: list[str],
+ source_tx_id: str, receipt: Any, mint_mode: str, intended_recipient: str) -> Receipt:
+ """Brief §3.2 confirm: find the xReserve ``DepositedToRemote`` log, re-verify every field, derive nonce/payload/hash."""
+ from web3.logs import DISCARD
+
+ Web3 = _web3().Web3
+ # Kept even though send() already asserted success: _recover_xreserve_from_history reaches
+ # this with receipts nothing has checked, so the revert test must live here too.
+ if int(receipt["status"]) == 0:
+ raise BridgeError(f"EVM transaction reverted: {source_tx_id}")
+ xreserve = self._contract(q.xreserve_contract, XRESERVE_ABI)
+ events = [ev for ev in xreserve.events.DepositedToRemote().process_receipt(receipt, errors=DISCARD)
+ if Web3.to_checksum_address(ev["address"]) == q.xreserve_contract]
+ if not events:
+ raise BridgeError("Confirmed receipt does not contain a valid DepositedToRemote event")
+
+ def matches(args: Mapping[str, Any]) -> bool:
+ return (Web3.to_checksum_address(args["localToken"]) == q.token
+ and Web3.to_checksum_address(args["localDepositor"]) == owner
+ and int(args["value"]) == q.amount_atomic
+ and int(args["remoteDomain"]) == q.remote_domain
+ and bytes(args["remoteRecipient"]) == q.remote_recipient_bytes32
+ and bytes(args["remoteToken"]) == q.remote_token_bytes32
+ and int(args["maxFee"]) == q.max_fee_atomic
+ and bytes(args["hookData"]) == q.hook_data)
+
+ # One transaction can batch several accounts' deposits, so take OUR event rather than the
+ # last one: every one of the eight canonical fields has to match for it to be ours.
+ ev = next((e for e in events if matches(e["args"])), None)
+ if ev is None:
+ raise BridgeError("DepositedToRemote event does not match the prepared transfer")
+ a = ev["args"]
+ log_index = int(ev["logIndex"])
+ if log_index < 0:
+ raise BridgeError("DepositedToRemote log index is missing or invalid")
+ # xReserve identifies a deposit by (source domain, tx hash, log index); the ordered payload is
+ # what Circle signs, so its keccak is the only safe attestation lookup key.
+ nonce = encoding.xreserve_deposit_nonce(q.source_domain, bytes.fromhex(source_tx_id[2:]), log_index)
+ payload = encoding.xreserve_deposit_payload(
+ amount=int(a["value"]), remote_domain=int(a["remoteDomain"]), remote_token=bytes(a["remoteToken"]),
+ remote_recipient=bytes(a["remoteRecipient"]), local_token=Web3.to_checksum_address(a["localToken"]),
+ depositor=Web3.to_checksum_address(a["localDepositor"]), max_fee=int(a["maxFee"]), nonce=nonce,
+ hook_data=bytes(a["hookData"]))
+ message_hash = "0x" + encoding.xreserve_message_hash(payload).hex()
+ state = self._xreserve_protocol_state(route, q, approval_tx_ids=approval_tx_ids, sender=owner,
+ mint_mode=mint_mode, intended_recipient=intended_recipient)
+ state.update({"sourceDomain": q.source_domain, "remoteDomain": q.remote_domain, "depositLogIndex": log_index,
+ "nonce": "0x" + nonce.hex(), "payload": "0x" + payload.hex(), "messageHash": message_hash,
+ "bridgeProgram": q.bridge_program, "wrapperProgram": q.wrapper_program})
+ return Receipt(id=message_hash, protocol="xreserve", status=Status.ATTESTATION_PENDING,
+ source_tx_id=source_tx_id, protocol_state=state)
+
+ def _xreserve_result(self, route: Route, q: _XReserveQuote, outcome: EvmOutcome, *, mint_mode: str,
+ intended_recipient: str) -> DepositReceipt:
+ approvals = list(outcome.approval_tx_ids)
+ if outcome.status == "CONFIRMED":
+ receipt = self._confirmed_deposit_receipt(route, q, owner=outcome.sender, approval_tx_ids=approvals,
+ source_tx_id=outcome.source_tx_id, receipt=outcome.receipt,
+ mint_mode=mint_mode, intended_recipient=intended_recipient)
+ return DepositReceipt(transaction_id=outcome.source_tx_id, route_id=route.id, message_hash=receipt.id,
+ nonce=receipt.protocol_state["nonce"], receipt=receipt)
+ rid = outcome.source_tx_id or approvals[-1]
+ receipt = Receipt(id=rid, protocol="xreserve", status=Status(outcome.status), source_tx_id=outcome.source_tx_id,
+ protocol_state=self._xreserve_protocol_state(route, q, approval_tx_ids=approvals, sender=outcome.sender,
+ mint_mode=mint_mode, intended_recipient=intended_recipient))
+ return DepositReceipt(transaction_id=rid, route_id=route.id, message_hash="", nonce="", receipt=receipt)
+
+ def deposit_usdc(self, recipient: str | None = None, *, amount: Any = None, amount_atomic: int | None = None,
+ mint_mode: str | None = None, secret_nonce: str = "0scalar",
+ plan: Plan | None = None) -> EvmCall[DepositReceipt]:
+ """Deposit USDC into Circle xReserve for USDCx on Aleo (minimum 2 USDC; irreversible once confirmed).
+
+ ``mint_mode``: ``public`` (public USDCx balance), ``record`` (protocol-minted private
+ record), or ``private`` (deposit addressed to the shielded wrapper program; you must later
+ run ``bridge.xreserve.private_mint`` / plan 4's ``complete`` with the same ``secret_nonce``,
+ which the SDK never stores). Approves exactly the amount only when the allowance is
+ short, then ``depositToRemote`` with no ``msg.value``. The confirmed ``DepositReceipt``
+ carries Circle's message hash (receipt id) and the deposit nonce. ``mint_mode`` defaults to
+ ``plan.mint_mode`` when a plan is given, else ``"public"``.
+
+ ``plan=`` executes a plan prepared earlier (typically ``quote.plan``): the route is
+ re-resolved by id against the live registry, the sender must be the connected account, and
+ the plan must equal what this call would have prepared itself. ``secret_nonce`` is never
+ part of a plan, so a private deposit must still pass the same one it was quoted with.
+ """
+ if plan is None and recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ if plan is not None:
+ route, sender, recipient, atomic, mint_mode = self._from_plan(
+ plan, "xreserve", recipient=recipient, amount=amount, amount_atomic=amount_atomic,
+ mint_mode=mint_mode, require_signer=True)
+ else:
+ route = self._xreserve_route()
+ mint_mode = "public" if mint_mode is None else mint_mode
+ sender = self.conn.require_address()
+ atomic = self._amount_atomic(route, amount, amount_atomic)
+ plan = _plan_for(self.registry, route, amount_atomic=atomic, recipient=recipient, sender=sender, mint_mode=mint_mode)
+ latest: dict[str, _XReserveQuote] = {}
+
+ def steps(owner: str) -> list[EvmStep]:
+ q = self._quote_xreserve(route, recipient, atomic, owner, mint_mode, secret_nonce) # fresh balance/allowance
+ latest["q"] = q
+ out: list[EvmStep] = []
+ if q.allowance_atomic < atomic:
+ out.append(EvmStep("approve", q.token, self._erc20(q.token).encode_abi("approve", args=[q.xreserve_contract, atomic])))
+ xreserve = self._contract(q.xreserve_contract, XRESERVE_ABI)
+ out.append(EvmStep("main", q.xreserve_contract, xreserve.encode_abi(
+ "depositToRemote", args=[atomic, q.remote_domain, q.remote_recipient_bytes32, q.token, q.max_fee_atomic, q.hook_data]), 0))
+ return out
+
+ def finish(outcome: EvmOutcome) -> DepositReceipt:
+ return self._xreserve_result(route, latest["q"], outcome, mint_mode=mint_mode, intended_recipient=recipient)
+
+ return EvmCall(self.conn, plan=plan, registry=self.registry, steps=steps, finish=finish, store=self.bridge.checkpoints)
+
+ # -- status ---------------------------------------------------------------------------------
+
+ @staticmethod
+ def _require_hash(value: Any, what: str) -> str:
+ if not isinstance(value, str) or not _HASH_RE.fullmatch(value):
+ raise CheckpointInvalidError(f"Receipt is missing a valid {what}")
+ return value
+
+ @staticmethod
+ def _failed(receipt: Receipt, key: str, message: str) -> Receipt:
+ return receipt.replace(status=Status.FAILED, next_action=None,
+ protocol_state={**receipt.protocol_state, key: message})
+
+ def _validate_hyperlane_state(self, route: Route, plan: Plan, receipt: Receipt) -> bytes:
+ """Bind every value that affects the dispatch before trusting checkpointed transaction ids."""
+ state = receipt.protocol_state
+ recipient32 = encoding.aleo_address_to_bytes32(plan.recipient)
+ if (receipt.protocol != "hyperlane"
+ or state.get("destinationDomain") != self._hyperlane_metadata(route).destination_domain
+ or state.get("amountAtomic") != str(plan.amount_atomic)
+ or not isinstance(state.get("recipientBytes32"), str)
+ or state["recipientBytes32"].lower() != "0x" + recipient32.hex()):
+ raise CheckpointInvalidError("Hyperlane checkpoint does not match the prepared transfer")
+ ids = state.get("approvalTxIds", [])
+ if not isinstance(ids, list) or any(not isinstance(i, str) or not _HASH_RE.fullmatch(i) for i in ids):
+ raise CheckpointInvalidError("Hyperlane checkpoint contains invalid approval transaction ids")
+ return recipient32
+
+ def _hyperlane_source_status(self, route: Route, plan: Plan, receipt: Receipt) -> Receipt:
+ self._validate_hyperlane_state(route, plan, receipt)
+ self.assert_chain(route)
+ source_tx_id = self._require_hash(receipt.source_tx_id, "source transaction id")
+ observed = self.conn.get_receipt(source_tx_id)
+ if observed is None:
+ return receipt
+ if int(observed["status"]) == 0:
+ return self._failed(receipt, "sourceError", f"EVM transaction reverted: {source_tx_id}")
+ message_id = self._message_id_from_receipt(route, observed)
+ state = dict(receipt.protocol_state)
+ if message_id is not None:
+ state["messageId"] = message_id
+ return receipt.replace(id=message_id or source_tx_id, status=Status.DELIVERY_PENDING, protocol_state=state)
+
+ def _xreserve_quote_from_state(self, route: Route, plan: Plan, receipt: Receipt) -> _XReserveQuote:
+ """veil ``resumeQuote``: rebuild the deposit arguments from saved state and bind them to the plan."""
+ Web3 = _web3().Web3
+ s = receipt.protocol_state
+ if receipt.protocol != "xreserve":
+ raise CheckpointInvalidError("Checkpoint does not match the prepared xReserve route")
+ if s.get("mintMode") != plan.mint_mode or s.get("intendedRecipient") != plan.recipient:
+ raise CheckpointInvalidError("Checkpoint does not match the prepared xReserve recipient")
+ try:
+ ok = (Web3.is_address(s["xReserveContract"]) and Web3.is_address(s["tokenAddress"])
+ and isinstance(s["sourceChainId"], int) and isinstance(s["remoteDomain"], int)
+ and _HASH_RE.fullmatch(s["remoteRecipientBytes32"]) is not None
+ and isinstance(s["hookData"], str) and len(s["hookData"]) == 132 and s["hookData"].startswith("0x")
+ and str(s["amountAtomic"]).isdigit() and str(s["maxFeeAtomic"]).isdigit())
+ except (KeyError, TypeError):
+ ok = False
+ if not ok:
+ raise CheckpointInvalidError("Checkpoint contains invalid xReserve submission state")
+ ids = s.get("approvalTxIds", [])
+ if not isinstance(ids, list) or any(not isinstance(i, str) or not _HASH_RE.fullmatch(i) for i in ids):
+ raise CheckpointInvalidError("Checkpoint contains invalid xReserve approval transaction ids")
+ meta = self._xreserve_metadata(route) # reuse the registry validator rather than trusting raw metadata again
+ return _XReserveQuote(
+ xreserve_contract=Web3.to_checksum_address(s["xReserveContract"]), token=Web3.to_checksum_address(s["tokenAddress"]),
+ source_chain_id=int(s["sourceChainId"]), source_domain=meta.source_domain, remote_domain=int(s["remoteDomain"]),
+ remote_token_bytes32=meta.remote_token_bytes32,
+ remote_recipient_bytes32=bytes.fromhex(s["remoteRecipientBytes32"][2:]), amount_atomic=int(s["amountAtomic"]),
+ max_fee_atomic=int(s["maxFeeAtomic"]), hook_data=bytes.fromhex(s["hookData"][2:]), balance_atomic=0, allowance_atomic=0,
+ bridge_program=meta.bridge_program, wrapper_program=meta.wrapper_program)
+
+ def _observed_owner(self, plan: Plan, receipt: Receipt | None) -> str:
+ """Prefer the sender committed to the receipt or plan so read-only recovery never needs a signer."""
+ Web3 = _web3().Web3
+ saved = receipt.protocol_state.get("sourceSender") if receipt is not None else None
+ for candidate in (saved, plan.sender, self.conn.address):
+ if isinstance(candidate, str) and Web3.is_address(candidate):
+ return Web3.to_checksum_address(candidate)
+ raise ConfigurationError("Read-only EVM access requires the prepared sender address (plan.sender or protocol_state.sourceSender)")
+
+ def _xreserve_source_status(self, route: Route, plan: Plan, receipt: Receipt) -> Receipt:
+ q = self._xreserve_quote_from_state(route, plan, receipt)
+ self.assert_chain(route)
+ owner = self._observed_owner(plan, receipt)
+ source_tx_id = self._require_hash(receipt.source_tx_id, "xReserve source transaction id")
+ observed = self.conn.get_receipt(source_tx_id)
+ if observed is None:
+ return receipt
+ if int(observed["status"]) == 0:
+ return self._failed(receipt, "sourceError", f"EVM transaction reverted: {source_tx_id}")
+ return self._confirmed_deposit_receipt(route, q, owner=owner,
+ approval_tx_ids=list(receipt.protocol_state.get("approvalTxIds", [])),
+ source_tx_id=source_tx_id, receipt=observed, mint_mode=plan.mint_mode,
+ intended_recipient=plan.recipient)
+
+ def source_status(self, plan: Plan, receipt: Receipt) -> Receipt:
+ """One read-only refresh of an Ethereum source leg (brief §2.4 branches 1 and 3, plus xReserve SOURCE_CONFIRMING).
+
+ ``SOURCE_APPROVAL_PENDING``: approval receipt → ``SOURCE_SUBMISSION_PENDING`` (or ``FAILED`` on revert).
+ ``SOURCE_CONFIRMING``: Hyperlane → ``DELIVERY_PENDING`` with the ``DispatchId`` message id;
+ xReserve → ``ATTESTATION_PENDING`` after re-verifying the ``DepositedToRemote`` event.
+ An unmined transaction returns the receipt unchanged. Never signs.
+
+ Deviation from veil (deliberate): veil raises for a reverted Hyperlane/xReserve *source*
+ transaction but returns ``FAILED`` for a reverted approval. Here every reverted source
+ transaction observed at this stage becomes ``FAILED`` with ``protocol_state["sourceError"]`` —
+ one uniform rule that plan 4's ``get_status``/``wait`` can rely on without a protocol switch.
+ ``send()`` still raises on revert.
+ """
+ route = self._route_for_plan(plan)
+ if receipt.protocol != plan.protocol or receipt.protocol_state.get("routeId") != plan.route_id:
+ raise CheckpointInvalidError("Receipt does not match the prepared route")
+ if receipt.status == Status.SOURCE_APPROVAL_PENDING:
+ approval_id = self._require_hash(receipt.id, "EVM approval transaction id")
+ observed = self.conn.get_receipt(approval_id)
+ if observed is None:
+ return receipt
+ if int(observed["status"]) == 0:
+ return self._failed(receipt, "sourceError", f"EVM approval transaction reverted: {approval_id}")
+ return receipt.replace(status=Status.SOURCE_SUBMISSION_PENDING)
+ if receipt.status == Status.SOURCE_CONFIRMING:
+ if route.protocol == "hyperlane":
+ return self._hyperlane_source_status(route, plan, receipt)
+ return self._xreserve_source_status(route, plan, receipt)
+ raise BridgeError("source_status refreshes SOURCE_APPROVAL_PENDING and SOURCE_CONFIRMING receipts only; "
+ "use bridge.get_status for later stages")
+
+ # -- recovery -----------------------------------------------------------------------------
+
+ def _checkpoint_approvals(self, checkpoint: Checkpoint) -> list[str]:
+ approvals = list((checkpoint.source or {}).get("approvalTransactionIds", []))
+ if any(not isinstance(a, str) or not _HASH_RE.fullmatch(a) for a in approvals):
+ raise CheckpointInvalidError("Bridge checkpoint contains an invalid approval transaction id")
+ return approvals
+
+ def _approval_scan_block(self, approvals: list[str]) -> int | None:
+ """Highest block of a confirmed approval; an unresolved hash is skipped, a reverted one is an error."""
+ block: int | None = None
+ for approval in approvals:
+ observed = self.conn.get_receipt(approval)
+ if observed is None:
+ continue
+ if int(observed["status"]) == 0:
+ raise BridgeError(f"EVM transaction reverted: {approval}")
+ number = int(observed["blockNumber"])
+ block = number if block is None or number > block else block
+ return block
+
+ def _scan_logs(self, address: str, from_block: int) -> list[Any]:
+ """Every log of *address* from *from_block* to the head, read in bounded ascending chunks.
+
+ The head is read once so the scan terminates on a fixed range, and every request carries an
+ explicit ``fromBlock``/``toBlock``: an unbounded filter is what public RPCs reject or truncate,
+ and a truncated answer would silently read as "no dispatch/deposit was ever submitted".
+ """
+ errors = _provider_errors()
+ chunk = self.log_scan_chunk_blocks
+ try:
+ latest = int(self.conn.w3.eth.block_number)
+ except errors as exc:
+ raise BridgeError(f"Could not read the current block number to bound a log scan of {address}: {exc}") from exc
+ logs: list[Any] = []
+ start = from_block
+ while start <= latest:
+ end = min(start + chunk - 1, latest)
+ try:
+ logs.extend(self.conn.w3.eth.get_logs({"address": address, "fromBlock": start, "toBlock": end}))
+ except errors as exc:
+ raise BridgeError(
+ f"eth_getLogs failed for blocks {start}-{end} of {from_block}-{latest} on {address}: {exc}. "
+ f"Use a dedicated RPC endpoint, or a smaller EthModule(log_scan_chunk_blocks=...) "
+ f"than the current {chunk}.") from exc
+ start = end + 1
+ return logs
+
+ def _recover_hyperlane_from_history(self, route: Route, recipient32: bytes, receipt: Receipt, approvals: list[str],
+ *, required: bool) -> Receipt | None:
+ """Scan router ``SentTransferRemote`` logs after the last confirmed approval; sender and router must match."""
+ from web3.exceptions import TransactionNotFound
+
+ Web3 = _web3().Web3
+ sender = receipt.protocol_state.get("sourceSender")
+ if not isinstance(sender, str) or not Web3.is_address(sender):
+ if required:
+ raise BridgeError("Cannot safely resume Hyperlane without the source account used by the approval")
+ return None
+ from_block = self._approval_scan_block(approvals)
+ if from_block is None:
+ if required:
+ raise BridgeError("Cannot safely resume Hyperlane because no confirmed approval block is available "
+ "for source history verification")
+ return None
+ amount = int(receipt.protocol_state["amountAtomic"])
+ meta = self._hyperlane_metadata(route) # reuse the registry validator before touching the router
+ destination = meta.destination_domain
+ router = meta.router
+ warp = self._contract(router, WARP_ROUTE_ABI)
+ topic = Web3.keccak(text="SentTransferRemote(uint32,bytes32,uint256)")
+ candidates: list[str] = []
+ for log in self._scan_logs(router, from_block):
+ if not log["topics"] or bytes(log["topics"][0]) != bytes(topic):
+ continue
+ args = warp.events.SentTransferRemote().process_log(log)["args"]
+ tx_hash = Web3.to_hex(log["transactionHash"])
+ if (int(args["destination"]) == destination and bytes(args["recipient"]) == recipient32
+ and int(args["amount"]) == amount and tx_hash not in candidates):
+ candidates.append(tx_hash)
+ matches: list[Receipt] = []
+ for tx_hash in candidates:
+ try:
+ tx = self.conn.w3.eth.get_transaction(tx_hash)
+ except TransactionNotFound:
+ continue
+ observed = self.conn.get_receipt(tx_hash)
+ if (tx is None or observed is None or tx["to"] is None
+ or Web3.to_checksum_address(tx["from"]) != Web3.to_checksum_address(sender)
+ or Web3.to_checksum_address(tx["to"]) != router):
+ continue
+ if int(observed["status"]) == 0:
+ # Deliberately asymmetric with the xReserve scan below, and identical to veil: a
+ # reverted Hyperlane candidate raises (hyperlane/evm.ts) because sender+router+args
+ # already identify it as ours, while xreserve/evmToAleo.ts swallows a rejected
+ # candidate because the shared contract's logs are mostly other accounts' deposits.
+ raise BridgeError(f"EVM transaction reverted: {tx_hash}")
+ message_id = self._message_id_from_receipt(route, observed)
+ state = dict(receipt.protocol_state)
+ if message_id is not None:
+ state["messageId"] = message_id
+ matches.append(receipt.replace(id=message_id or tx_hash, status=Status.DELIVERY_PENDING,
+ source_tx_id=tx_hash, protocol_state=state))
+ if len(matches) > 1:
+ raise BridgeError("Multiple matching Hyperlane dispatches were found; recovery cannot safely choose one source transaction")
+ return matches[0] if matches else None
+
+ def _recover_hyperlane(self, route: Route, plan: Plan, checkpoint: Checkpoint, *, required: bool) -> Receipt:
+ Web3 = _web3().Web3
+ recipient32 = encoding.aleo_address_to_bytes32(plan.recipient)
+ approvals = self._checkpoint_approvals(checkpoint)
+ sender = Web3.to_checksum_address(plan.sender) if plan.sender and Web3.is_address(plan.sender) else None
+ meta = self._hyperlane_metadata(route)
+ state = self._hyperlane_protocol_state(route, recipient_bytes32=recipient32,
+ destination_domain=meta.destination_domain,
+ native_value_atomic=0, amount_atomic=plan.amount_atomic,
+ approval_tx_ids=approvals, sender=sender)
+ transaction_id = (checkpoint.source or {}).get("transactionId")
+ if not transaction_id:
+ if not approvals:
+ raise CheckpointInvalidError("Bridge checkpoint contains no submitted transaction")
+ pending = Receipt(id=approvals[-1], protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING, protocol_state=state)
+ observed = self.conn.get_receipt(approvals[-1])
+ if observed is None:
+ if required:
+ raise BridgeError("Cannot safely resume Hyperlane because no confirmed approval block is available "
+ "for source history verification")
+ return pending
+ if int(observed["status"]) == 0:
+ return self._failed(pending, "sourceError", f"EVM approval transaction reverted: {approvals[-1]}")
+ recovered = self._recover_hyperlane_from_history(route, recipient32, pending, approvals, required=required)
+ return recovered or pending.replace(status=Status.SOURCE_SUBMISSION_PENDING)
+ transaction_id = self._require_hash(transaction_id, "source transaction id")
+ pending = Receipt(id=transaction_id, protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=transaction_id, protocol_state=state)
+ observed = self._hyperlane_source_status(route, plan, pending)
+ if observed is not pending:
+ return observed
+ return self._recover_hyperlane_from_history(route, recipient32, pending, approvals, required=False) or observed
+
+ def _recover_xreserve_from_history(self, route: Route, plan: Plan, q: _XReserveQuote, owner: str, approvals: list[str],
+ *, required: bool) -> Receipt | None:
+ """Scan xReserve logs after the last confirmed approval; a candidate matches only if every event field matches."""
+ Web3 = _web3().Web3
+ from_block = self._approval_scan_block(approvals)
+ if from_block is None:
+ if required:
+ raise BridgeError("Cannot safely resume xReserve because no confirmed approval block is available "
+ "for source history verification")
+ return None
+ hashes: list[str] = []
+ for log in self._scan_logs(q.xreserve_contract, from_block):
+ tx_hash = Web3.to_hex(log["transactionHash"])
+ if tx_hash not in hashes:
+ hashes.append(tx_hash)
+ matches: list[Receipt] = []
+ for tx_hash in hashes:
+ observed = self.conn.get_receipt(tx_hash)
+ if observed is None:
+ continue
+ try:
+ matches.append(self._confirmed_deposit_receipt(route, q, owner=owner, approval_tx_ids=approvals, source_tx_id=tx_hash,
+ receipt=observed, mint_mode=plan.mint_mode, intended_recipient=plan.recipient))
+ except BridgeError:
+ continue # other accounts' deposits share the contract; unrelated unless every field matches
+ if len(matches) > 1:
+ raise BridgeError("Multiple matching xReserve deposits were found; recovery cannot safely choose one source transaction")
+ return matches[0] if matches else None
+
+ def _recover_xreserve(self, route: Route, plan: Plan, checkpoint: Checkpoint, *, required: bool) -> Receipt:
+ Web3 = _web3().Web3
+ owner = self._observed_owner(plan, None)
+ approvals = self._checkpoint_approvals(checkpoint)
+ source = checkpoint.source or {}
+ stored_hook = source.get("hookData")
+ if stored_hook is not None and (not isinstance(stored_hook, str) or not re.fullmatch(r"0x[0-9a-fA-F]{130}", stored_hook)):
+ raise CheckpointInvalidError("Bridge checkpoint contains invalid xReserve hook data")
+ hook = bytes.fromhex(stored_hook[2:]) if stored_hook else encoding.xreserve_hook_data(
+ plan.mint_mode, plan.recipient, self.network, "0scalar")
+ meta = self._xreserve_metadata(route) # reuse the registry validator rather than trusting raw metadata
+ token = self.registry.asset(route.source_asset_id).locator
+ if token is None or token.kind != "evm-contract":
+ raise RouteUnavailableError(f"xReserve source token contract is missing: {route.id}")
+ q = _XReserveQuote(
+ xreserve_contract=meta.xreserve_contract, token=Web3.to_checksum_address(token.value),
+ source_chain_id=meta.source_chain_id, source_domain=meta.source_domain, remote_domain=meta.remote_domain,
+ remote_token_bytes32=meta.remote_token_bytes32,
+ remote_recipient_bytes32=self._xreserve_recipient_bytes32(route, meta, plan.recipient, plan.mint_mode),
+ amount_atomic=plan.amount_atomic, max_fee_atomic=meta.max_fee_atomic, hook_data=hook,
+ balance_atomic=0, allowance_atomic=0, bridge_program=meta.bridge_program, wrapper_program=meta.wrapper_program)
+ state = self._xreserve_protocol_state(route, q, approval_tx_ids=approvals, sender=owner, mint_mode=plan.mint_mode,
+ intended_recipient=plan.recipient)
+ transaction_id = source.get("transactionId")
+ if not transaction_id:
+ if not approvals:
+ raise CheckpointInvalidError("Bridge checkpoint contains no submitted transaction")
+ pending = Receipt(id=approvals[-1], protocol="xreserve", status=Status.SOURCE_APPROVAL_PENDING, protocol_state=state)
+ observed = self.conn.get_receipt(approvals[-1])
+ if observed is None:
+ if required:
+ raise BridgeError("Cannot safely resume xReserve because no confirmed approval block is available "
+ "for source history verification")
+ return pending
+ if int(observed["status"]) == 0:
+ return self._failed(pending, "sourceError", f"EVM approval transaction reverted: {approvals[-1]}")
+ recovered = self._recover_xreserve_from_history(route, plan, q, owner, approvals, required=required)
+ return recovered or pending.replace(status=Status.SOURCE_SUBMISSION_PENDING)
+ transaction_id = self._require_hash(transaction_id, "xReserve source transaction id")
+ pending = Receipt(id=transaction_id, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=transaction_id, protocol_state=state)
+ observed = self._xreserve_source_status(route, plan, pending)
+ if observed is not pending:
+ return observed
+ return self._recover_xreserve_from_history(route, plan, q, owner, approvals, required=False) or observed
+
+ def recover_source(self, plan: Plan, checkpoint: Checkpoint, *, required: bool = False) -> Receipt:
+ """Reconstruct an interrupted Ethereum source leg from a checkpoint without signing (brief §2.7, §3.1, §3.2).
+
+ Approval-only checkpoints: observe the last approval; when confirmed, scan the router /
+ xReserve logs from its block for a matching dispatch or deposit and stop at
+ ``SOURCE_SUBMISSION_PENDING`` when none exists — recovery never moves funds. Checkpoints
+ with a source transaction are observed through ``source_status``. ``required=True`` (plan
+ 4's resume-before-dispatch mode) demands the scan actually run — a known sender and a
+ confirmed approval block — or raises, instead of quietly returning an approval-boundary
+ receipt. ``required=True`` only makes the INABILITY to scan fatal: once the scan actually
+ runs, a completed scan that matches zero dispatches/deposits is a valid answer ("nothing
+ was submitted yet"), not an error, and returns ``SOURCE_SUBMISSION_PENDING`` so ``resume``
+ may re-authorize the send.
+ """
+ if checkpoint.version != 1 or checkpoint.intent.get("bridgeProtocol") != plan.protocol or checkpoint.route.get("id") != plan.route_id:
+ raise CheckpointInvalidError("Bridge checkpoint does not match the prepared route")
+ if checkpoint.route.get("registryVersion") != self.registry.version:
+ raise RegistryVersionMismatchError(
+ f"Checkpoint uses registry {checkpoint.route.get('registryVersion')}; this client has {self.registry.version}")
+ if plan.protocol == "hyperlane" and checkpoint.destination:
+ raise CheckpointInvalidError("Hyperlane checkpoints must not carry a destination leg")
+ route = self._route_for_plan(plan)
+ self.assert_chain(route)
+ if route.protocol == "hyperlane":
+ return self._recover_hyperlane(route, plan, checkpoint, required=required)
+ if route.protocol == "xreserve":
+ return self._recover_xreserve(route, plan, checkpoint, required=required)
+ raise UnsupportedRouteError(f"No Ethereum recovery for protocol {route.protocol}")
+
+ def _mailbox_address(self) -> str:
+ """The Hyperlane Mailbox deployed on this chain.
+
+ Prefer a route that ORIGINATES here and passes the full metadata validator: its
+ ``mailboxAddress`` is the contract this chain's own dispatches go through, checksummed and
+ checked. Only if no such route exists do we fall back to any route that merely touches this
+ chain (whose ``mailboxAddress`` may be the remote one, and is unvalidated).
+ """
+ routes = list(self.registry.routes(protocol="hyperlane", include_unavailable=True,
+ environment=self.bridge.environment))
+ for route in routes:
+ if self.registry.asset(route.source_asset_id).chain_id != self.chain.id:
+ continue
+ try:
+ return self._hyperlane_metadata(route).mailbox
+ except (ConfigurationError, RouteUnavailableError):
+ continue
+ for route in routes:
+ chains = {self.registry.asset(route.source_asset_id).chain_id, self.registry.asset(route.destination_asset_id).chain_id}
+ mailbox = route.metadata.get("mailboxAddress")
+ if self.chain.id in chains and isinstance(mailbox, str):
+ return mailbox
+ raise UnsupportedRouteError(f"No Hyperlane Mailbox is configured for {self.chain.id} in registry {self.registry.version}")
+
+ def is_delivered(self, message_id: str | bytes) -> bool:
+ """``Mailbox.delivered(bytes32)`` on this chain — the canonical Aleo → Ethereum delivery signal."""
+ raw = bytes.fromhex(message_id[2:]) if isinstance(message_id, str) and message_id.startswith("0x") else message_id
+ if not isinstance(raw, (bytes, bytearray)) or len(raw) != 32:
+ raise BridgeError("Hyperlane delivery requires a 32-byte message id")
+ return bool(self._contract(self._mailbox_address(), MAILBOX_ABI).functions.delivered(bytes(raw)).call())
+
+ def balance(self, asset: Any, *, address: str | None = None) -> int:
+ """Atomic balance of ``asset`` (native via ``eth_getBalance``, ERC-20 via ``balanceOf``) for ``address`` or the connection's account."""
+ target = self._asset(asset)
+ owner = self._owner(address)
+ if owner is None:
+ raise ConfigurationError("balance() needs an address: pass address= or configure a signer")
+ if target.locator is None:
+ raise UnsupportedRouteError(f"{target.id} has no on-chain locator")
+ if target.locator.kind == "native":
+ return int(self.conn.w3.eth.get_balance(owner))
+ if target.locator.kind == "evm-contract":
+ return int(self._erc20(target.locator.value).functions.balanceOf(owner).call())
+ raise UnsupportedRouteError(f"{target.id} is not an EVM asset")
+
+ def _chain_assertion_route(self) -> Route:
+ """Any route originating on this chain with a usable ``sourceChainId``, used only to bind
+ ``assert_chain`` to the registry's notion of this chain (never touches contracts)."""
+ routes = [r for r in self.registry.routes(include_unavailable=True, environment=self.bridge.environment)
+ if self.registry.asset(r.source_asset_id).chain_id == self.chain.id
+ and isinstance(r.metadata.get("sourceChainId"), int)]
+ if not routes:
+ raise UnsupportedRouteError(
+ f"No Hyperlane or xReserve route with sourceChainId is configured for {self.chain.id}")
+ return routes[0]
+
+ def chain_status(self) -> ChainStatus:
+ """Address, signing ability, and atomic balances of every registry asset on this chain (empty when read-only).
+
+ Asserts the connected ``Web3``'s ``eth_chainId`` matches this chain's registry
+ ``sourceChainId`` first, so a connection pointed at the wrong network raises
+ ``ChainMismatchError`` instead of silently reading balances from the wrong chain.
+ """
+ self.assert_chain(self._chain_assertion_route())
+ address = self.conn.address
+ balances: dict[str, int] = {}
+ if address is not None:
+ for asset in self.registry.assets(chain=self.chain.id):
+ if asset.locator is not None and asset.locator.kind in ("native", "evm-contract"):
+ balances[asset.id] = self.balance(asset, address=address)
+ return ChainStatus(chain_id=self.chain.id, address=address, can_sign=self.conn.can_sign, balances=balances)
+
+
+__all__ = ["Ethereum", "EthModule"]
diff --git a/bridge-sdk/python/aleo_bridge/freezelist.py b/bridge-sdk/python/aleo_bridge/freezelist.py
new file mode 100644
index 0000000..050f418
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/freezelist.py
@@ -0,0 +1,193 @@
+"""Sealance compliance tree — Merkle exclusion proofs for ARC-22 (USDCx) private transfers and burns.
+
+Port of ``sdk/src/integrations/sealance/merkle-tree.ts``. Leaves are frozen addresses as little-endian
+field ints, sorted ascending and front-padded with ``0field`` to a power of two (minimum two). The leaf
+level hashes ``Poseidon4([1field, l, r])``, inner levels ``Poseidon4([0field, l, r])`` — with the
+three-element array packed through ``Plaintext.to_fields()``, exactly as the Leo program does. A sibling
+path starts with the leaf itself, then one sibling per level, zero-padded to ``depth`` entries.
+The deployed ``MerkleProof`` struct is ``[field; 16]`` + ``u32``, so proofs use ``PROOF_SIBLINGS = 16``.
+"""
+from __future__ import annotations
+
+import math
+from typing import TYPE_CHECKING
+
+from . import encoding as enc
+from .errors import ConfigurationError
+
+if TYPE_CHECKING: # pragma: no cover
+ from .client import Bridge
+
+ZERO_ADDRESS = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
+DEFAULT_DEPTH = 15 # TS getSiblingPath default; tree capacity 2**(depth-1) leaves
+PROOF_SIBLINGS = 16 # struct MerkleProof { siblings: [field; 16u32], leaf_index: u32 }
+FREEZE_LIST_MAPPING = "freeze_list" # address => bool (frozen flag, NOT the list)
+FREEZE_LIST_LAST_INDEX_MAPPING = "freeze_list_last_index" # bool => u32, keyed "true"
+FREEZE_LIST_INDEX_MAPPING = "freeze_list_index" # u32 => address (the ordered list; [0u32] = zero-address sentinel)
+FREEZE_LIST_ROOT_MAPPING = "freeze_list_root" # u8 => field, keyed "1u8" (current) / "2u8" (previous)
+CURRENT_ROOT_KEY = "1u8"
+# Token program -> its freeze-list program, for when Bridge.program(token).imports is unavailable.
+FREEZE_LIST_PROGRAMS = {
+ "usdcx_stablecoin.aleo": "usdcx_freezelist.aleo",
+ "test_usdcx_stablecoin.aleo": "test_usdcx_freezelist.aleo",
+}
+EMPTY_TREE_ROOT = 3642222252059314292809609689035560016959342421640560347114299934615987159853
+_EMPTY_PROOF = "{ siblings: [" + ", ".join(["0field"] * PROOF_SIBLINGS) + "], leaf_index: 1u32 }"
+EMPTY_MERKLE_PROOF_PAIR = f"[{_EMPTY_PROOF}, {_EMPTY_PROOF}]"
+
+
+def address_to_field_int(address: str) -> int:
+ """bech32m payload bytes read little-endian — the field element a Leo program sees for an address."""
+ return int.from_bytes(enc.aleo_address_to_bytes32(address), "little")
+
+
+def hash_two(prefix: str, left: str, right: str, network: str) -> str:
+ """``Poseidon4(Plaintext("[prefix,left,right]").to_fields())`` as a ``…field`` literal."""
+ net = enc.network_module(network)
+ plaintext = net.Plaintext.from_string(f"[{prefix},{left},{right}]")
+ return str(net.Poseidon4().hash(plaintext.to_fields()))
+
+
+def generate_leaves(addresses: list[str], depth: int = DEFAULT_DEPTH) -> list[str]:
+ """Frozen addresses → sorted ``…field`` leaves, zero address dropped, front-padded to a power of two (min 2)."""
+ live = [a for a in addresses if a != ZERO_ADDRESS]
+ max_leaves = 2 ** (depth - 1)
+ if len(live) > max_leaves:
+ raise ConfigurationError(f"Leaves limit exceeded. Max: {max_leaves}, provided: {len(live)}")
+ count = 2 if len(live) <= 1 else 2 ** math.ceil(math.log2(len(live)))
+ fields = sorted(address_to_field_int(a) for a in live)
+ return ["0field"] * (count - len(fields)) + [f"{f}field" for f in fields]
+
+
+def build_tree(leaves: list[str], network: str) -> list[int]:
+ """Bottom-up tree as ints: leaves first, root last (the layout the TS SDK and compliance API use)."""
+ if not leaves:
+ raise ConfigurationError("Leaves array cannot be empty")
+ if len(leaves) % 2:
+ raise ConfigurationError("Leaves array must have even number of elements")
+ tree = list(leaves)
+ level = list(leaves)
+ while len(level) > 1:
+ prefix = "1field" if len(level) == len(leaves) else "0field"
+ level = [hash_two(prefix, level[i], level[i + 1], network) for i in range(0, len(level), 2)]
+ tree.extend(level)
+ return [int(node[: -len("field")]) for node in tree]
+
+
+def leaf_indices(tree: list[int], address: str) -> tuple[int, int]:
+ """(left, right) leaf indices bracketing *address* for a non-inclusion proof (TS ``getLeafIndices``)."""
+ count = (len(tree) + 1) // 2
+ target = address_to_field_int(address)
+ leaves = tree[:count]
+ right = next((i for i, leaf in enumerate(leaves) if target <= leaf), -1)
+ left = right - 1
+ if right == -1:
+ right = left = count - 1
+ if right == 0:
+ left = 0
+ return left, right
+
+
+def sibling_path(tree: list[int], index: int, depth: int = DEFAULT_DEPTH) -> list[int]:
+ """Leaf, then the sibling at each level, zero-padded to *depth* entries (TS ``getSiblingPath``)."""
+ count = (len(tree) + 1) // 2
+ path = [tree[index]]
+ node, parent, level = index, count, 1
+ while parent < len(tree):
+ sibling = node + 1 if node % 2 == 0 else node - 1
+ path.append(tree[sibling])
+ node = parent + index // 2 ** level
+ parent += count // 2 ** level
+ level += 1
+ while len(path) < depth:
+ path.append(0)
+ return path
+
+
+def format_merkle_proof(left: tuple[list[int], int], right: tuple[list[int], int]) -> str:
+ """``[MerkleProof; 2]`` literal with veil's spacing: ``{ siblings: [a, b], leaf_index: Nu32 }``."""
+ parts = []
+ for siblings, index in (left, right):
+ parts.append("{ siblings: [" + ", ".join(f"{s}field" for s in siblings) + f"], leaf_index: {index}u32 }}")
+ return "[" + ", ".join(parts) + "]"
+
+
+class FreezeList:
+ """``bridge.freezelist`` — reads a compliant token's frozen addresses and proves an address is not among them."""
+
+ def __init__(self, bridge: "Bridge") -> None:
+ self._bridge = bridge
+
+ def freeze_list_program(self, token_program: str) -> str:
+ """The freeze-list program backing *token_program* (usually a token program that imports it)."""
+ if token_program.endswith("freezelist.aleo"):
+ return token_program
+ from aleo.facade.errors import AleoError, ProgramNotFound
+ try:
+ imports = self._bridge.program(token_program).imports
+ except (ProgramNotFound, AleoError):
+ imports = []
+ for dep in imports:
+ if str(dep).endswith("freezelist.aleo"):
+ return str(dep)
+ fallback = FREEZE_LIST_PROGRAMS.get(token_program)
+ if fallback:
+ return fallback
+ raise ConfigurationError(f"{token_program} has no freeze-list program; pass merkle_proof explicitly")
+
+ def leaves(self, program: str) -> list[str]:
+ """Frozen addresses from *program*'s freeze-list ``freeze_list_index`` mapping (0..last inclusive),
+ with the zero-address sentinel dropped; ``[]`` when the list is empty/unreadable."""
+ fl_program = self.freeze_list_program(program)
+ last = self._bridge.mapping_value(fl_program, FREEZE_LIST_LAST_INDEX_MAPPING, "true")
+ if last is None:
+ return []
+ try:
+ count = int(last.removesuffix("u32"))
+ except ValueError as exc:
+ raise ConfigurationError(f"{fl_program}/{FREEZE_LIST_LAST_INDEX_MAPPING} returned {last!r}, expected a u32") from exc
+ addresses = []
+ for index in range(count + 1):
+ value = self._bridge.mapping_value(fl_program, FREEZE_LIST_INDEX_MAPPING, f"{index}u32")
+ if value and value != ZERO_ADDRESS:
+ addresses.append(value)
+ return addresses
+
+ def _verified_tree(self, fl_program: str, leaves: list[str]) -> list[int]:
+ tree = build_tree(generate_leaves(leaves), self._bridge.network)
+ on_chain_root = self._bridge.mapping_value(fl_program, FREEZE_LIST_ROOT_MAPPING, CURRENT_ROOT_KEY)
+ if on_chain_root is None:
+ raise ConfigurationError(
+ f"{fl_program}/{FREEZE_LIST_ROOT_MAPPING}[{CURRENT_ROOT_KEY}] is unreadable; pass merkle_proof= explicitly")
+ computed_root = f"{tree[-1]}field"
+ if computed_root != on_chain_root:
+ raise ConfigurationError(
+ f"computed freeze-list root {computed_root} != on-chain root {on_chain_root} for {fl_program}; "
+ "refusing to build a proof")
+ return tree
+
+ def tree(self, program: str) -> list[int]:
+ """Merkle tree over *program*'s frozen addresses, verified against the on-chain root when readable."""
+ return self._verified_tree(self.freeze_list_program(program), self.leaves(program))
+
+ def exclusion_proof(self, address: str, program: str) -> str:
+ """``[MerkleProof; 2]`` proving *address* is not frozen on *program*; veil's empty pair when the list is empty."""
+ fl_program = self.freeze_list_program(program)
+ leaves = self.leaves(program)
+ tree = self._verified_tree(fl_program, leaves) # verifies the on-chain root, empty list included
+ if not leaves:
+ return EMPTY_MERKLE_PROOF_PAIR
+ count = (len(tree) + 1) // 2
+ target = address_to_field_int(address)
+ if target in tree[:count]:
+ raise ConfigurationError(f"{address} is on the {program} freeze list; no exclusion proof exists for it")
+ left, right = leaf_indices(tree, address)
+ return format_merkle_proof((sibling_path(tree, left, PROOF_SIBLINGS), left),
+ (sibling_path(tree, right, PROOF_SIBLINGS), right))
+
+
+__all__ = ["CURRENT_ROOT_KEY", "DEFAULT_DEPTH", "EMPTY_MERKLE_PROOF_PAIR", "EMPTY_TREE_ROOT",
+ "FREEZE_LIST_INDEX_MAPPING", "FREEZE_LIST_LAST_INDEX_MAPPING", "FREEZE_LIST_MAPPING",
+ "FREEZE_LIST_PROGRAMS", "FREEZE_LIST_ROOT_MAPPING", "PROOF_SIBLINGS", "ZERO_ADDRESS", "FreezeList",
+ "address_to_field_int", "build_tree", "format_merkle_proof", "generate_leaves", "hash_two",
+ "leaf_indices", "sibling_path"]
diff --git a/bridge-sdk/python/aleo_bridge/hyperlane.py b/bridge-sdk/python/aleo_bridge/hyperlane.py
new file mode 100644
index 0000000..bd03808
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/hyperlane.py
@@ -0,0 +1,189 @@
+"""Aleo-origin Hyperlane warp routes (port of veil protocols/hyperlane/aleo.ts and utils/hyperlaneDelivery.ts).
+
+Seven-input ``transfer_remote`` with allowance slot 0 = live IGP payment; IGP quote from
+``hyp_hook_manager.aleo/destination_gas_configs``; delivery read from ``hyp_mailbox.aleo/deliveries``.
+"""
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Any
+
+from . import encoding as enc
+from ._calls import AleoCall
+from .errors import (AmbiguousRouteError, ConfigurationError, InvalidAmountError, InvalidRecipientError,
+ RouteNotFoundError, RouteUnavailableError, UnsupportedRouteError)
+from .registry import Asset, Route
+from .types import DispatchReceipt, GasQuote, Receipt, Status
+from .units import format_decimal_amount, parse_decimal_amount, resolve_amount
+
+if TYPE_CHECKING: # pragma: no cover
+ from .client import Bridge
+
+MAX_U64 = (1 << 64) - 1
+GAS_QUOTE_SCALE = 10_000_000_000 # fixed by hyp_hook_manager.aleo post_dispatch
+ZERO_GAS_LIMIT_FALLBACK = 50_000
+_GAS_FIELDS = ("gas_overhead", "exchange_rate", "gas_price")
+_GAS_FIELD_RE = re.compile(r"\b(gas_overhead|exchange_rate|gas_price)\s*:\s*(\d+)u128")
+
+
+def parse_gas_config(literal: str) -> dict[str, int]:
+ """``{ gas_overhead: 159337u128, exchange_rate: 402u128, gas_price: 1000000000u128 }`` → ints."""
+ found = {m.group(1): int(m.group(2)) for m in _GAS_FIELD_RE.finditer(literal)}
+ missing = [f for f in _GAS_FIELDS if f not in found]
+ if missing:
+ raise ConfigurationError(f"Hyperlane destination gas configuration is malformed (missing {missing}): {literal!r}")
+ return found
+
+
+def gas_config_key(route: Route) -> str:
+ """The ``destination_gas_configs`` key the hook manager reads at finalization."""
+ return f"{{ igp: {route.meta_str('aleoMailboxDefaultHook')}, destination: {route.meta_int('aleoDestinationDomain')}u32 }}"
+
+
+def compute_gas_payment(*, gas_limit: int, gas_overhead: int, gas_price: int, exchange_rate: int) -> int:
+ """Exact integer formula asserted on chain: ``(limit + overhead) * price * rate // 10^10`` as a positive u64."""
+ payment = ((gas_limit + gas_overhead) * gas_price * exchange_rate) // GAS_QUOTE_SCALE
+ if payment <= 0 or payment > MAX_U64:
+ raise ConfigurationError(f"Hyperlane hook payment does not fit a positive u64: {payment}")
+ return payment
+
+
+def _allowance(route: Route, index: int, amount: int | None) -> str:
+ value = str(amount) if amount is not None else route.meta_str(f"aleoAllowanceAmount{index}")
+ return f"{{ spender: {route.meta_str(f'aleoAllowanceSpender{index}')}, amount: {value}u64 }}"
+
+
+class HyperlaneModule:
+ """``bridge.hyperlane`` — Aleo-side Hyperlane reads and the ``transfer_remote`` write."""
+
+ def __init__(self, bridge: "Bridge") -> None:
+ self._bridge = bridge
+
+ # ── route resolution ──
+ def _aleo_asset(self, asset: Any) -> Asset:
+ resolved = self._bridge.registry.asset(asset)
+ if resolved.chain_id != self._bridge.aleo_chain().id:
+ raise UnsupportedRouteError(
+ f"{resolved.id} is not an Aleo asset on {self._bridge.environment}; Aleo-origin Hyperlane transfers "
+ "start from aleo/eth, aleo/wbtc, aleo/usdt or aleo/sol (use bridge.eth / bridge.sol for other origins)")
+ return resolved
+
+ def _route_for(self, asset_or_route: Any) -> Route:
+ if isinstance(asset_or_route, Route):
+ route = asset_or_route
+ if route.protocol != "hyperlane":
+ raise UnsupportedRouteError(f"Not a Hyperlane route: {route.id}")
+ self._aleo_asset(route.source_asset_id)
+ return route
+ return self.outbound_route(asset_or_route)
+
+ def outbound_route(self, asset: Any) -> Route:
+ """The single active, non-placeholder Hyperlane route leaving this Aleo asset."""
+ source = self._aleo_asset(asset)
+ candidates = [r for r in self._bridge.registry.routes(protocol="hyperlane", include_unavailable=True,
+ environment=self._bridge.environment)
+ if r.source_asset_id == source.id]
+ if not candidates:
+ raise RouteNotFoundError(f"No Hyperlane route leaves {source.id}")
+ executable = [r for r in candidates if r.active and r.metadata.get("aleoPlaceholderConfiguration") is not True]
+ if not executable:
+ detail = ", ".join(f"{r.id} ({r.availability})" for r in candidates)
+ raise RouteUnavailableError(f"Hyperlane routes from {source.id} are not executable: {detail}")
+ if len(executable) > 1:
+ raise AmbiguousRouteError(f"Several active Hyperlane routes leave {source.id}: {[r.id for r in executable]}")
+ return executable[0]
+
+ # ── reads ──
+ def quote_gas_payment(self, asset: Any) -> GasQuote:
+ """Live relayer payment for the route (the exact u64 the hook asserts); quote right before proving."""
+ route = self._route_for(asset)
+ literal = self._bridge.mapping_value(route.meta_str("aleoHookManagerProgram"), "destination_gas_configs",
+ gas_config_key(route))
+ if literal is None:
+ raise ConfigurationError(f"Hyperlane destination gas configuration is missing on chain: {route.id}")
+ config = parse_gas_config(literal)
+ if config["exchange_rate"] == 0 or config["gas_price"] == 0:
+ raise ConfigurationError(f"Hyperlane destination gas configuration is unpriced: {route.id}")
+ gas_limit = int(route.meta_str("aleoRemoteRouterGas")) or ZERO_GAS_LIMIT_FALLBACK
+ payment = compute_gas_payment(gas_limit=gas_limit, gas_overhead=config["gas_overhead"],
+ gas_price=config["gas_price"], exchange_rate=config["exchange_rate"])
+ return GasQuote(route.id, gas_limit, config["gas_overhead"], config["gas_price"], config["exchange_rate"], payment)
+
+ def _mailbox_program(self) -> str:
+ for route in self._bridge.registry.routes(protocol="hyperlane", include_unavailable=True, environment=self._bridge.environment):
+ program = route.metadata.get("aleoMailboxProgram")
+ if isinstance(program, str) and program:
+ return program
+ raise ConfigurationError(f"No Aleo Hyperlane mailbox program is configured for {self._bridge.environment}")
+
+ def is_delivered(self, message_id: "str | bytes") -> bool:
+ """Whether ``hyp_mailbox.aleo/deliveries`` holds the message (mapping presence is the acceptance signal)."""
+ try:
+ raw = enc.hex_to_bytes(message_id, 32)
+ except (ValueError, InvalidRecipientError) as exc:
+ raise ConfigurationError("Hyperlane delivery requires a 32-byte message id") from exc
+ return self._bridge.mapping_value(self._mailbox_program(), "deliveries", enc.hyperlane_delivery_key(raw)) is not None
+
+ # ── transfer_remote ──
+ def build_transfer_remote_inputs(self, route: Route, *, recipient: str, amount_atomic: int,
+ gas_payment_microcredits: int, decimals: tuple[int, int]) -> list[str]:
+ """The seven ``transfer_remote`` literals (brief §3.3). Pure; works for placeholder routes too (inspection only)."""
+ if isinstance(gas_payment_microcredits, bool) or not isinstance(gas_payment_microcredits, int) \
+ or not (0 < gas_payment_microcredits <= MAX_U64):
+ raise ConfigurationError(f"gas_payment_microcredits must be a positive u64: {gas_payment_microcredits}")
+ if amount_atomic <= 0:
+ raise InvalidAmountError("Bridge transfer amount must be greater than zero")
+ registry = self._bridge.registry
+ destination = registry.asset(route.destination_asset_id)
+ destination_chain = registry.chain(destination.chain_id)
+ if not destination.matches_address(recipient):
+ raise InvalidRecipientError(f"Recipient does not match the {destination.chain_id} address format: {recipient}")
+ if destination_chain.family == "evm":
+ limbs = enc.evm_address_to_hyperlane_recipient(recipient)
+ elif destination_chain.family == "solana":
+ limbs = enc.solana_address_to_hyperlane_recipient(recipient)
+ else:
+ raise UnsupportedRouteError(f"Unsupported Hyperlane destination family {destination_chain.family!r}: {route.id}")
+ local_decimals, remote_decimals = decimals
+ domain = route.meta_int("aleoDestinationDomain")
+ app_metadata = (f"{{ token_type: {route.meta_str('aleoTokenType')}u8, token_owner: {route.meta_str('aleoTokenOwner')}, "
+ f"ism: {route.meta_str('aleoIsm')}, hook: {route.meta_str('aleoHook')}, "
+ f"token_id: {route.meta_str('aleoTokenId')}, local_decimals: {local_decimals}u8, "
+ f"remote_decimals: {remote_decimals}u8 }}")
+ mailbox_state = (f"{{ default_hook: {route.meta_str('aleoMailboxDefaultHook')}, "
+ f"required_hook: {route.meta_str('aleoMailboxRequiredHook')} }}")
+ remote_router = (f"{{ domain: {domain}u32, recipient: {route.meta_str('aleoRemoteRouterRecipient')}, "
+ f"gas: {route.meta_str('aleoRemoteRouterGas')}u128 }}")
+ allowances = "[" + ", ".join(_allowance(route, i, gas_payment_microcredits if i == 0 else None) for i in range(4)) + "]"
+ return [app_metadata, mailbox_state, remote_router, f"{domain}u32", enc.u128_pair_literal(limbs),
+ f"{amount_atomic}u128", allowances]
+
+ def transfer_remote(self, asset: Any, recipient: str, *, amount: Any = None, amount_atomic: int | None = None,
+ as_signer: bool = False, gas_payment_microcredits: int | None = None) -> AleoCall[DispatchReceipt]:
+ """Withdraw an Aleo warp asset to Ethereum/Solana. Quotes the IGP payment now unless pinned; the
+ lifecycle layer (plan 4) re-quotes at the last responsible moment by calling this again."""
+ route = self.outbound_route(asset)
+ registry = self._bridge.registry
+ source, destination = registry.asset(route.source_asset_id), registry.asset(route.destination_asset_id)
+ atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=source.decimals)
+ parse_decimal_amount(format_decimal_amount(atomic, source.decimals), destination.decimals) # veil prepare(): representable on both sides
+ payment = gas_payment_microcredits if gas_payment_microcredits is not None \
+ else self.quote_gas_payment(route).payment_microcredits
+ decimals = (route.meta_int("aleoLocalDecimals", source.decimals), route.meta_int("aleoRemoteDecimals", destination.decimals))
+ inputs = self.build_transfer_remote_inputs(route, recipient=recipient, amount_atomic=atomic,
+ gas_payment_microcredits=payment, decimals=decimals)
+ program = route.meta_str("aleoRouterProgram")
+ function = "transfer_remote_as_signer" if as_signer else "transfer_remote"
+
+ def build(tx_id: str, _outputs: list[str]) -> DispatchReceipt:
+ receipt = Receipt(id=tx_id, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id,
+ protocol_state={"routeId": route.id, "sourceProgram": program, "sourceFunction": function,
+ "amountAtomic": str(atomic), "recipient": recipient,
+ "gasPaymentMicrocredits": str(payment)})
+ return DispatchReceipt(transaction_id=tx_id, route_id=route.id, message_id=None, amount_atomic=atomic, receipt=receipt)
+
+ return self._bridge._call(program, function, inputs, build)
+
+
+__all__ = ["GAS_QUOTE_SCALE", "MAX_U64", "ZERO_GAS_LIMIT_FALLBACK", "HyperlaneModule", "compute_gas_payment",
+ "gas_config_key", "parse_gas_config"]
diff --git a/bridge-sdk/python/aleo_bridge/lifecycle.py b/bridge-sdk/python/aleo_bridge/lifecycle.py
new file mode 100644
index 0000000..d2a567b
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/lifecycle.py
@@ -0,0 +1,1389 @@
+"""Tier 1 lifecycle verbs — veil's ``quote → execute → wait`` with ``recover`` /
+``resume`` / ``complete`` driven by ``Progress.next``.
+
+Every function takes the registry (and, from later tasks on, the
+:class:`~aleo_bridge.client.Bridge`) and touches only its public surface, so the
+unit suite can run them against plain fakes without a network. ``Bridge`` binds
+thin methods of the same names.
+
+Order of the file follows the caller journey: planning (``prepare``), pricing
+(``quote``), committing funds (``execute``), observing (``get_status``,
+``wait``), and recovery (``recover``, ``resume``, ``complete``) — this task
+only adds ``prepare`` and the ``resolve_route`` helper every later verb shares.
+"""
+from __future__ import annotations
+
+import json
+import re
+import time
+from dataclasses import dataclass, replace
+from typing import Any, Callable
+
+from . import _sealevel
+from ._calls import is_duplicate_submission
+from .encoding import HOOK_DATA_BYTES
+from ._plan import build_plan
+from .checkpoint import Checkpoint, create_checkpoint
+from .errors import (
+ AttestationError,
+ BridgeError,
+ CheckpointInvalidError,
+ ConfigurationError,
+ DeliveryUnknownError,
+ InvalidAmountError,
+ InvalidRecipientError,
+ NotResumableError,
+ PollingTimeoutError,
+ RegistryVersionMismatchError,
+ RouteUnavailableError,
+ UnsupportedRouteError,
+)
+from .registry import Asset, Chain, Registry, Route
+from .types import (CALLER_BOUNDARIES, TERMINAL, AleoHyperlaneQuote, AleoXReserveQuote, Attestation,
+ Fee, Plan, Progress, Quote, Receipt, Status, to_progress)
+from .units import format_decimal_amount, parse_decimal_amount, resolve_amount
+
+MINT_MODES = ("public", "record", "private")
+
+
+# ── Route resolution (invariant 1) ────────────────────────────────────────────
+
+@dataclass(frozen=True)
+class ResolvedRoute:
+ """The live registry entries behind one plan — re-resolved on every verb."""
+
+ route: Route
+ source_asset: Asset
+ destination_asset: Asset
+ source_chain: Chain
+ destination_chain: Chain
+
+
+def resolve_route(registry: Registry, plan: Plan) -> ResolvedRoute:
+ """Re-resolve *plan* against the live registry; refuse stale or altered plans.
+
+ Raises :class:`RegistryVersionMismatchError` when the plan was built from a
+ different registry version (re-quote to fix) and
+ :class:`CheckpointInvalidError` when its route topology no longer matches.
+ """
+ if plan.registry_version != registry.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has "
+ f"{registry.version}. Re-run quote() to rebuild the plan.")
+ route = registry.route(plan.route_id)
+ if (route.protocol != plan.protocol
+ or route.source_asset_id != plan.source_asset_id
+ or route.destination_asset_id != plan.destination_asset_id):
+ raise CheckpointInvalidError(
+ f"Plan route {plan.route_id} does not match the configured registry "
+ "(protocol or asset pair differs). Re-run quote().")
+ source = registry.asset(route.source_asset_id)
+ destination = registry.asset(route.destination_asset_id)
+ return ResolvedRoute(route, source, destination,
+ registry.chain(source.chain_id), registry.chain(destination.chain_id))
+
+
+def _require_active(route: Route) -> None:
+ if route.availability != "active":
+ raise RouteUnavailableError(
+ f"Route {route.id} is '{route.availability}': it is listed by the registry "
+ "but cannot move funds until its deployment is reviewed. Pick an active route "
+ "(bridge.registry.routes()).")
+
+
+# ── prepare ───────────────────────────────────────────────────────────────────
+
+def prepare(registry: Registry, *, source, destination, amount=None, amount_atomic=None,
+ recipient: str, sender: str | None = None, protocol: str | None = None,
+ mint_mode: str = "public") -> Plan:
+ """Describe how *amount* of *source* moves to *destination* — pure, no network.
+
+ Resolves the single non-disabled route for the asset pair (``protocol``
+ disambiguates), validates the mint mode (non-public only for xReserve into
+ Aleo), parses the amount with the source decimals AND re-parses it with the
+ destination decimals so no precision is silently lost, regex-checks the
+ recipient against the destination chain, then hands off to
+ :func:`aleo_bridge._plan.build_plan` for the step list — the same builder
+ ``bridge.eth.*`` / ``bridge.sol.*`` use, so a caller-supplied plan and a
+ ``prepare()``-built one are always identical for the same route and amount.
+ Nothing is signed and no chain is contacted; ``quote`` adds live prices on
+ top of this.
+ """
+ src = registry.asset(source)
+ dst = registry.asset(destination)
+ route = registry.find_route(src.id, dst.id, protocol)
+ dst_chain = registry.chain(dst.chain_id)
+
+ if mint_mode not in MINT_MODES:
+ raise ConfigurationError(f"mint_mode must be one of {MINT_MODES}, got {mint_mode!r}")
+ if mint_mode != "public" and dst_chain.family != "aleo":
+ raise ConfigurationError(
+ "Aleo mint mode is only valid when the destination chain is Aleo")
+ if route.protocol != "xreserve" and mint_mode != "public":
+ raise ConfigurationError(
+ "record and private mint modes are only supported by xReserve routes")
+
+ atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=src.decimals)
+ if atomic <= 0:
+ raise InvalidAmountError("Bridge transfer amount must be greater than zero")
+ parse_decimal_amount(format_decimal_amount(atomic, src.decimals), dst.decimals) # destination precision check
+
+ if dst.address_regex and not re.fullmatch(dst.address_regex, recipient):
+ raise InvalidRecipientError(
+ f"Recipient {recipient!r} does not match the {dst.chain_id} address format "
+ f"({dst.address_regex})")
+
+ return build_plan(registry, route, amount_atomic=atomic, recipient=recipient,
+ sender=sender, mint_mode=mint_mode)
+
+
+# ── Connection helpers ────────────────────────────────────────────────────────
+
+def _module(bridge, name: str):
+ """``bridge.eth`` / ``bridge.sol`` or a ConfigurationError that says how to fix it.
+
+ Checks the ``ethereum``/``solana`` connection attribute FIRST: on the real
+ ``Bridge``, ``eth``/``sol`` are properties that themselves raise
+ ``ConfigurationError`` when unconfigured, so ``getattr(bridge, name, None)``
+ would never see the ``None`` default — it would let that raise propagate
+ with the property's own (less specific) message instead of this one.
+ """
+ conn = getattr(bridge, "ethereum" if name == "eth" else "solana", None)
+ if conn is None:
+ chain, extra, env = (("Ethereum", "evm", "ETHEREUM_RPC_URL / EVM_PRIVATE_KEY") if name == "eth"
+ else ("Solana", "solana", "SOLANA_RPC_URL / SOLANA_PRIVATE_KEY"))
+ raise ConfigurationError(
+ f"This transfer needs a configured {chain} connection: pass "
+ f"{'ethereum' if name == 'eth' else 'solana'}= to Bridge(...) (pip install "
+ f"'aleo-bridge-sdk[{extra}]') or set {env} for Bridge.from_env().")
+ return getattr(bridge, name)
+
+
+def _credits_asset_id(chain: Chain) -> str:
+ return f"{chain.id}/aleo"
+
+
+# ── quote ─────────────────────────────────────────────────────────────────────
+
+def quote(bridge, *, source, destination, amount=None, amount_atomic=None, recipient: str,
+ sender: str | None = None, protocol: str | None = None, mint_mode: str = "public",
+ secret_nonce: str = "0scalar") -> Quote:
+ """Price a transfer: ``prepare`` + the source-side live read for the route kind.
+
+ Returns one of ``EvmHyperlaneQuote`` / ``SolanaHyperlaneQuote`` /
+ ``AleoHyperlaneQuote`` / ``EvmXReserveQuote`` / ``AleoXReserveQuote``
+ (``quote.kind``), each carrying the canonical ``plan`` that ``execute`` takes.
+ Aleo-origin xReserve quotes make no network call (fixed withdrawal fee).
+ Nothing is signed.
+
+ Dispatches to ``bridge.eth``/``bridge.sol`` with ``plan=`` (never a re-derived
+ ``asset``/``recipient``/``amount_atomic`` form): those modules re-resolve the
+ route from the plan and validate it against the live registry themselves. The
+ quote they return is then re-stamped with this function's own ``plan`` (via
+ ``replace``) so the plan on the result is always exactly what ``prepare()``
+ built, regardless of what the module attached internally.
+ """
+ plan = prepare(bridge.registry, source=source, destination=destination, amount=amount,
+ amount_atomic=amount_atomic, recipient=recipient, sender=sender,
+ protocol=protocol, mint_mode=mint_mode)
+ resolved = resolve_route(bridge.registry, plan)
+ _require_active(resolved.route)
+ family = resolved.source_chain.family
+
+ if plan.protocol == "hyperlane" and family == "evm":
+ q = _module(bridge, "eth").quote_transfer_remote(plan=plan)
+ return replace(q, plan=plan)
+ if plan.protocol == "hyperlane" and family == "solana":
+ q = _module(bridge, "sol").quote_transfer_remote(plan=plan)
+ return replace(q, plan=plan)
+ if plan.protocol == "hyperlane" and family == "aleo":
+ gas = bridge.hyperlane.quote_gas_payment(plan.source_asset_id)
+ fee = Fee(kind="protocol", chain_id=resolved.source_chain.id,
+ asset_id=_credits_asset_id(resolved.source_chain),
+ amount=format_decimal_amount(gas.payment_microcredits, 6), estimated=True)
+ return AleoHyperlaneQuote(kind="aleo-hyperlane", plan=plan, fees=(fee,), amount_out=plan.amount,
+ gas_limit=gas.gas_limit, gas_overhead=gas.gas_overhead,
+ gas_price=gas.gas_price, exchange_rate=gas.exchange_rate,
+ payment_microcredits=gas.payment_microcredits)
+ if plan.protocol == "xreserve" and family == "evm":
+ q = _module(bridge, "eth").quote_deposit_usdc(plan=plan, secret_nonce=secret_nonce)
+ return replace(q, plan=plan)
+ if plan.protocol == "xreserve" and family == "aleo":
+ raw = resolved.route.metadata.get("withdrawalFeeAtomic")
+ if not isinstance(raw, str) or not raw.isdigit():
+ raise RouteUnavailableError(f"xReserve withdrawal fee is missing or invalid: {plan.route_id}")
+ fee_atomic = int(raw)
+ decimals = resolved.source_asset.decimals
+ fee_human = format_decimal_amount(fee_atomic, decimals)
+ if plan.amount_atomic <= fee_atomic:
+ raise InvalidAmountError(
+ f"xReserve burn amount must exceed the {fee_human} {resolved.source_asset.symbol} "
+ f"withdrawal fee (got {plan.amount})")
+ return AleoXReserveQuote(
+ kind="aleo-xreserve", plan=plan,
+ fees=(Fee(kind="protocol", chain_id=resolved.source_chain.id, asset_id=resolved.source_asset.id,
+ amount=fee_human, estimated=False),),
+ amount_out=format_decimal_amount(plan.amount_atomic - fee_atomic, decimals),
+ withdrawal_fee_atomic=fee_atomic)
+ raise UnsupportedRouteError(
+ f"Unsupported {plan.protocol} source chain family: {family} ({plan.route_id})")
+
+
+# ── Checkpoint emission ───────────────────────────────────────────────────────
+
+def _persist(bridge, checkpoint: Checkpoint, receipt: Receipt, *, previous_id: str | None = None) -> None:
+ """Mirror *checkpoint* into the bound store: save (or drop, if terminal) BEFORE superseding
+ the previous id — never the reverse, so a crash between the two steps still leaves a valid
+ record for the transfer rather than a moment where the store holds neither.
+ """
+ store = getattr(bridge, "checkpoints", None)
+ if store is None:
+ return
+ if receipt.status in TERMINAL:
+ store.delete(checkpoint.id)
+ else:
+ store.save(checkpoint)
+ if previous_id is not None and previous_id != checkpoint.id:
+ store.delete(previous_id)
+
+
+class _Emitter:
+ """Turns receipts into checkpoints: caller callback first, then the bound store.
+
+ Two channels feed it — a protocol module's own ``on_checkpoint`` (which hands over a
+ ``Checkpoint`` it has already reduced, and saves ITSELF only after this call returns) and
+ ``execute``'s own emission once the send returns. A boundary that arrives through both is
+ handed to the caller once: the two reductions compare equal, being the same receipt reduced
+ against the same plan.
+
+ A module-emitted checkpoint supersedes the previous id before the module has actually saved
+ the new one — deleting the old id here (save-then-delete, brief §review item 6) would leave a
+ window where the store holds neither if it crashed. So that delete is parked as
+ ``_pending_supersede`` and only carried out once we know the module's save has landed: at the
+ start of the next emission (module-emitted or not — the loop that owns the module has already
+ returned from its ``store.save`` by then) or, failing that, when ``execute`` calls
+ :meth:`finalize` after its own last receipt is persisted.
+ """
+
+ def __init__(self, bridge, plan: Plan, on_checkpoint: Callable | None) -> None:
+ self._bridge, self._plan, self._cb = bridge, plan, on_checkpoint
+ self._last_id: str | None = None
+ self._last: Checkpoint | None = None
+ self._pending_supersede: str | None = None
+
+ def __call__(self, receipt) -> Checkpoint:
+ self._flush_pending()
+ module_emitted = isinstance(receipt, Checkpoint)
+ checkpoint = receipt if module_emitted else create_checkpoint(self._plan, receipt, self._bridge.registry)
+ if checkpoint != self._last:
+ if hasattr(self._bridge, "events"):
+ # test hook: FakeBridge records the ordering of proving/checkpoint/broadcast
+ label = receipt.status.value if isinstance(receipt, Receipt) else "module"
+ self._bridge.events.append((f"checkpoint:{label}", checkpoint.id))
+ if self._cb is not None:
+ self._cb(checkpoint) # the caller's own callback: errors are theirs
+ if module_emitted:
+ self._pending_supersede = self._last_id # module saves this one itself, after we return
+ else:
+ _persist(self._bridge, checkpoint, receipt, previous_id=self._last_id)
+ self._last_id, self._last = checkpoint.id, checkpoint
+ return checkpoint
+
+ def _flush_pending(self) -> None:
+ if self._pending_supersede is None:
+ return
+ pending, self._pending_supersede = self._pending_supersede, None
+ if pending == self._last_id:
+ return
+ store = getattr(self._bridge, "checkpoints", None)
+ if store is not None:
+ store.delete(pending)
+
+ def finalize(self) -> None:
+ """Drop any still-pending supersede. Call once execute()'s final receipt is persisted."""
+ self._flush_pending()
+
+
+# ── Execution helpers ─────────────────────────────────────────────────────────
+
+def _assert_sender(plan: Plan, address: str | None, *, family: str) -> None:
+ """Refuse a plan prepared for a different account than the one that would sign it.
+
+ EVM addresses are hex and their checksum casing carries no identity, so they compare
+ case-insensitively; Solana addresses are base58, where case IS part of the address.
+ """
+ if not plan.sender or not address:
+ return
+ same = plan.sender.lower() == address.lower() if family == "evm" else plan.sender == address
+ if not same:
+ raise ConfigurationError(
+ f"Plan sender {plan.sender} does not match the connected account {address}. "
+ "Re-quote with sender=None or the connection's own address.")
+
+
+def _connected_aleo_address(bridge) -> str | None:
+ """``bridge.aleo_address()``, or None when no account is configured to sign with.
+
+ A read-only facade cannot sign an Aleo leg anyway, so a missing account is not this check's
+ problem to raise on — it just means there is nothing to compare the plan's sender against.
+ """
+ try:
+ return bridge.aleo_address()
+ except (ConfigurationError, AttributeError):
+ return None
+
+
+def _read_destination_balance(bridge, plan: Plan, resolved: ResolvedRoute) -> int | None:
+ """The recipient's destination balance, or None when there is no reader for it.
+
+ Only read when the destination connection IS the recipient (there is no per-address balance
+ read in the module contracts); otherwise return None rather than baseline the wrong account.
+
+ A transport failure is NOT swallowed here (Task 6 review item 8): ``get_status`` branch 6 uses
+ this balance as the delivery SIGNAL, and a swallowed RPC error would read as "not delivered
+ yet" forever instead of being retried by ``wait``'s transient classifier. The one caller that
+ genuinely cannot afford to raise — ``execute``'s advisory pre-broadcast baseline — does the
+ swallowing itself, in :func:`_delivery_verification`.
+ """
+ chain, asset = resolved.destination_chain, resolved.destination_asset
+ if chain.family == "evm":
+ conn = getattr(bridge, "ethereum", None)
+ if (conn is None or not conn.address
+ or conn.address.lower() != plan.recipient.lower()
+ or asset.locator is None or asset.locator.kind not in ("native", "evm-contract")):
+ return None
+ return int(bridge.eth.balance(asset.id))
+ if chain.family == "solana":
+ conn = getattr(bridge, "solana", None)
+ if (conn is None or conn.address != plan.recipient
+ or asset.locator is None or asset.locator.kind != "native"):
+ return None
+ return int(bridge.sol.balance())
+ return None # Aleo private records / token mappings: protocol signal instead
+
+
+def _delivery_verification(bridge, plan: Plan, resolved: ResolvedRoute) -> dict[str, str]:
+ """``execute``'s advisory delivery baseline — an unreadable balance is simply omitted.
+
+ The best-effort swallow lives at THIS call site and not inside ``_read_destination_balance``
+ (Task 6 review item 8): here the balance is a nice-to-have baseline written into a checkpoint
+ before broadcast, so a flaky RPC must never block funds movement; in ``get_status`` branch 6
+ the same read is the delivery signal and must raise.
+ """
+ try:
+ before = _read_destination_balance(bridge, plan, resolved)
+ except Exception: # noqa: BLE001 — advisory read only
+ return {}
+ if before is None:
+ return {}
+ expected = parse_decimal_amount(plan.amount, resolved.destination_asset.decimals)
+ return {"destinationBalanceBeforeAtomic": str(before),
+ "expectedDestinationIncreaseAtomic": str(expected)}
+
+
+def _prepare_aleo(call, proving: str):
+ if proving == "delegate":
+ return call.delegate_prepared()
+ if proving == "local":
+ return call.prove()
+ raise ConfigurationError(f"proving must be 'delegate' (DPS) or 'local', got {proving!r}")
+
+
+def _run_aleo_leg(bridge, plan: Plan, call, *, proving: str, emit: _Emitter,
+ extra_state: dict[str, Any]) -> Receipt:
+ """Invariant 3: prove → checkpoint the exact transaction → broadcast → checkpoint the id."""
+ prepared = _prepare_aleo(call, proving)
+ emit(Receipt(id=prepared.transaction_id, protocol=plan.protocol,
+ status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "preparedTransaction": prepared.serialized,
+ **extra_state}))
+ result = call.submit_prepared(prepared, wait=False) # polling is wait()'s job, not execute()'s
+ receipt: Receipt = result.receipt
+ receipt = receipt.replace(id=prepared.transaction_id, status=Status.SOURCE_CONFIRMING,
+ source_tx_id=prepared.transaction_id,
+ protocol_state={**receipt.protocol_state, "routeId": plan.route_id, **extra_state})
+ emit(receipt)
+ return receipt
+
+
+def _send_call(call, emit: _Emitter, poll_seconds: float, timeout_seconds: float) -> Receipt:
+ result = call.send(wait=True, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds,
+ on_checkpoint=emit)
+ receipt: Receipt = result.receipt
+ emit(receipt)
+ return receipt
+
+
+def _aleo_hyperlane_mode(mode: str | None) -> bool:
+ if mode is None or mode == "caller":
+ return False
+ if mode == "signer":
+ return True
+ raise ConfigurationError(f"Aleo Hyperlane mode must be 'caller' or 'signer', got {mode!r}")
+
+
+def _xreserve_burn_mode(mode: str | None) -> str:
+ if mode is None:
+ return "private"
+ if mode in ("private", "public", "public-as-signer"):
+ return mode
+ raise ConfigurationError(
+ f"Aleo xReserve mode must be 'private', 'public' or 'public-as-signer', got {mode!r}")
+
+
+def _mint_secret(plan: Plan, secret_nonce: str | None) -> str:
+ """The secret the EVM deposit commits to. A private mint must supply its own.
+
+ ``secret_nonce`` is not a ``Plan`` field and the SDK never stores it: a private deposit that
+ quietly fell back to the ``"0scalar"`` default would commit to a hook nobody can reproduce,
+ and ``complete`` needs the same value again to mint the record.
+ """
+ if plan.mint_mode == "private":
+ if not secret_nonce:
+ raise ConfigurationError(
+ "a secret_nonce is required for a private xReserve mint: the deposit commits to "
+ "(recipient, secret_nonce) and complete() needs the same value again — keep it, "
+ "the SDK never stores it")
+ return secret_nonce
+ return secret_nonce or "0scalar"
+
+
+# ── execute ───────────────────────────────────────────────────────────────────
+
+def execute(bridge, plan: Plan, *, on_checkpoint: Callable | None = None, proving: str = "delegate",
+ mode: str | None = None, record: str | None = None, merkle_proof: str | None = None,
+ gas_payment_microcredits: int | None = None, secret_nonce: str | None = None,
+ poll_seconds: float = 1.0, timeout_seconds: float = 120.0) -> Progress:
+ """Commit funds on the source chain and return the transfer's ``Progress``.
+
+ Runs every source-chain leg for ``plan`` — approval(s) → deposit / dispatch / burn — emitting
+ a ``Checkpoint`` at each boundary (after each approval hash; after proving and BEFORE
+ broadcast for Aleo legs; after broadcast). Aleo legs prove with ``proving="delegate"`` (DPS)
+ or ``"local"``; ``mode`` is ``"caller"|"signer"`` for Aleo Hyperlane and ``"private"|"public"|
+ "public-as-signer"`` for Aleo xReserve burns (``record`` / ``merkle_proof`` feed a private
+ burn). The Hyperlane hook payment is re-quoted right before proving unless
+ ``gas_payment_microcredits`` pins it. ``secret_nonce`` is the private-mint commitment secret
+ for an EVM xReserve deposit — required when ``plan.mint_mode == "private"``; keep it,
+ ``complete`` needs it again and the SDK never stores it.
+
+ EVM and Solana legs are dispatched through the module's own ``plan=`` surface, so the route,
+ registry version, sender and every plan field are re-validated by the module that builds the
+ transaction. Returns after broadcast; call ``wait`` to observe acceptance and delivery. Once
+ the irreversible step is broadcast, recover from the checkpoint — never re-run ``execute``.
+ """
+ resolved = resolve_route(bridge.registry, plan)
+ _require_active(resolved.route)
+ family = resolved.source_chain.family
+ emit = _Emitter(bridge, plan, on_checkpoint)
+
+ if plan.protocol == "hyperlane" and family == "evm":
+ eth = _module(bridge, "eth")
+ _assert_sender(plan, bridge.ethereum.address, family=family)
+ call = eth.transfer_remote(plan=plan)
+ receipt = _send_call(call, emit, poll_seconds, timeout_seconds)
+ emit.finalize()
+ return to_progress(plan, receipt)
+
+ if plan.protocol == "hyperlane" and family == "solana":
+ sol = _module(bridge, "sol")
+ _assert_sender(plan, bridge.solana.address, family=family)
+ call = sol.transfer_remote(plan=plan)
+ receipt = _send_call(call, emit, poll_seconds, timeout_seconds)
+ emit.finalize()
+ return to_progress(plan, receipt)
+
+ if plan.protocol == "hyperlane" and family == "aleo":
+ _assert_sender(plan, _connected_aleo_address(bridge), family="aleo")
+ as_signer = _aleo_hyperlane_mode(mode)
+ verification = _delivery_verification(bridge, plan, resolved)
+ gas = gas_payment_microcredits
+ if gas is None:
+ gas = bridge.hyperlane.quote_gas_payment(plan.source_asset_id).payment_microcredits
+ call = bridge.hyperlane.transfer_remote(plan.source_asset_id, plan.recipient,
+ amount_atomic=plan.amount_atomic, as_signer=as_signer,
+ gas_payment_microcredits=gas)
+ receipt = _run_aleo_leg(bridge, plan, call, proving=proving, emit=emit, extra_state=verification)
+ emit.finalize()
+ return to_progress(plan, receipt)
+
+ if plan.protocol == "xreserve" and family == "evm":
+ eth = _module(bridge, "eth")
+ _assert_sender(plan, bridge.ethereum.address, family=family)
+ nonce = _mint_secret(plan, secret_nonce)
+ call = eth.deposit_usdc(plan=plan, secret_nonce=nonce)
+ receipt = _send_call(call, emit, poll_seconds, timeout_seconds)
+ emit.finalize()
+ return to_progress(plan, receipt)
+
+ if plan.protocol == "xreserve" and family == "aleo":
+ _assert_sender(plan, _connected_aleo_address(bridge), family="aleo")
+ burn_mode = _xreserve_burn_mode(mode)
+ call = bridge.xreserve.burn(plan.recipient, amount_atomic=plan.amount_atomic, mode=burn_mode,
+ record=record, merkle_proof=merkle_proof)
+ receipt = _run_aleo_leg(bridge, plan, call, proving=proving, emit=emit, extra_state={})
+ emit.finalize()
+ return to_progress(plan, receipt)
+
+ raise UnsupportedRouteError(f"Unsupported {plan.protocol} source chain family: {family} ({plan.route_id})")
+
+
+# ── Aleo transaction status ───────────────────────────────────────────────────
+
+def aleo_transaction_status(bridge, tx_id: str) -> tuple[str, str | None]:
+ """``("accepted" | "rejected" | "pending", error)`` from the confirmed-transaction envelope.
+
+ Reads ``GET /transaction/confirmed/{id}`` through the facade. A 404
+ (``TransactionNotFound``) means not confirmed yet → ``pending``. The
+ envelope's top-level ``status`` is the node's verdict; it carries no reason,
+ so the error text is generic.
+ """
+ from aleo.facade.errors import TransactionNotFound
+ try:
+ confirmed = bridge.aleo.network.get_confirmed_transaction(tx_id)
+ except TransactionNotFound:
+ return "pending", None
+ status = confirmed.get("status") if isinstance(confirmed, dict) else getattr(confirmed, "status", None)
+ if status == "accepted":
+ return "accepted", None
+ if status == "rejected":
+ return "rejected", f"Aleo transaction {tx_id} was rejected by the network"
+ return "pending", None
+
+
+_HEX = re.compile(r"^0x[0-9a-fA-F]*$")
+_MESSAGE_ID_RE = re.compile(r"^0x[0-9a-fA-F]{64}$")
+
+
+def _hex_bytes(value: Any, *, length: int | None = None) -> bytes | None:
+ """Strict ``0x`` hex → bytes, or None when malformed / wrong length."""
+ if not isinstance(value, str) or not _HEX.match(value) or len(value) % 2:
+ return None
+ data = bytes.fromhex(value[2:])
+ if length is not None and len(data) != length:
+ return None
+ return data
+
+
+def _check_receipt(plan: Plan, receipt: Receipt) -> None:
+ if receipt.protocol != plan.protocol or receipt.protocol_state.get("routeId") != plan.route_id:
+ raise CheckpointInvalidError("Bridge receipt does not match the prepared route")
+
+
+def _clear_action(receipt: Receipt, **changes) -> Receipt:
+ return receipt.replace(next_action=None, **changes)
+
+
+def _message_id(receipt: Receipt) -> str | None:
+ """``protocol_state["messageId"]`` first; else ``receipt.id`` when it is itself a message id.
+
+ Solana and EVM Hyperlane receipts carry the id in ``protocol_state["messageId"]`` once
+ known; a receipt that instead carries the message id AS its own ``id`` (some Aleo-origin
+ shapes) falls back to that, but only when it is an exact 32-byte ``0x`` hex string — never a
+ signature or an unrelated transaction hash of a different width. Never falls back when
+ ``receipt.id == receipt.source_tx_id``: an EVM Hyperlane receipt whose DispatchId log was
+ unreadable carries the source transaction hash as its id, which is a same-shaped ``0x`` hex
+ string but is NOT a message id.
+ """
+ state = receipt.protocol_state
+ message_id = state.get("messageId")
+ if isinstance(message_id, str) and message_id:
+ return message_id
+ if (isinstance(receipt.id, str) and receipt.id != receipt.source_tx_id
+ and _MESSAGE_ID_RE.fullmatch(receipt.id)):
+ return receipt.id
+ return None
+
+
+# ── get_status ────────────────────────────────────────────────────────────────
+
+def get_status(bridge, plan: Plan, receipt: Receipt) -> Receipt:
+ """One refresh of the transfer's state — no polling, no signing.
+
+ Ports veil's branch table in order: terminal receipts (``COMPLETED``/``FAILED``/``EXPIRED``)
+ return untouched; EVM approvals and source confirmations delegate to the chain module (never
+ called for any other status — both ``EthModule.source_status`` and ``SolModule.source_status``
+ raise otherwise); Aleo source acceptance moves to ``DELIVERY_PENDING``; Hyperlane delivery is
+ read from the destination Mailbox by message id (filling a missing Solana message id from the
+ source transaction's logs first, never handing a signature to ``is_delivered``), or from the
+ destination balance baseline for Aleo-origin routes; inbound xReserve reads the destination
+ nullifier FIRST (invariant 6), then Circle's attestation (private mode stops at
+ ``DESTINATION_ACTION_REQUIRED`` with ``next_action`` = ``{"kind": "xreserve-private-mint",
+ "chainId": ...}``), then the private mint's acceptance. Returns the SAME object when nothing
+ changed.
+
+ A Solana transport failure inside ``SolModule.source_status`` (or the message-id log read) is
+ not swallowed here: a single refresh may raise on a flaky public RPC, and retrying with backoff
+ is ``wait``'s job, not this function's.
+ """
+ resolved = resolve_route(bridge.registry, plan)
+ _check_receipt(plan, receipt)
+ if receipt.status in TERMINAL:
+ return receipt
+ route, src, dst = resolved.route, resolved.source_chain, resolved.destination_chain
+ state = receipt.protocol_state
+
+ # 1. EVM approval → wallet boundary
+ if receipt.status is Status.SOURCE_APPROVAL_PENDING and src.family == "evm":
+ return _module(bridge, "eth").source_status(plan, receipt)
+
+ # 2. Aleo source acceptance is the irreversible boundary
+ if receipt.status is Status.SOURCE_CONFIRMING and src.family == "aleo":
+ if not receipt.source_tx_id:
+ raise CheckpointInvalidError("Bridge receipt is missing its Aleo source transaction id")
+ verdict, error = aleo_transaction_status(bridge, receipt.source_tx_id)
+ if verdict == "accepted":
+ return _clear_action(receipt, status=Status.DELIVERY_PENDING)
+ if verdict == "rejected":
+ return _clear_action(receipt, status=Status.FAILED,
+ protocol_state={**state, "sourceError": error})
+ return receipt
+
+ # 3/4. Hyperlane source confirmation on EVM / Solana (extracts messageId)
+ if receipt.status is Status.SOURCE_CONFIRMING and route.protocol == "hyperlane":
+ if src.family == "evm":
+ return _module(bridge, "eth").source_status(plan, receipt)
+ if src.family == "solana":
+ return _module(bridge, "sol").source_status(plan, receipt)
+
+ # 5. Hyperlane delivery: the destination Mailbox is canonical
+ message_id = _message_id(receipt)
+ if (message_id is None and receipt.status is Status.DELIVERY_PENDING and route.protocol == "hyperlane"
+ and src.family == "solana" and state.get("messageIdUnavailable") and receipt.source_tx_id):
+ sol = _module(bridge, "sol") # missing connection must raise, not be swallowed
+ try:
+ logs = sol._transaction_logs(receipt.source_tx_id)
+ except Exception: # noqa: BLE001 — advisory fill-in only
+ logs = None
+ filled = None if logs is None else _sealevel.extract_hyperlane_message_id(logs)
+ if filled is not None:
+ new_state = {k: v for k, v in state.items() if k != "messageIdUnavailable"}
+ new_state["messageId"] = filled
+ receipt = receipt.replace(id=filled, protocol_state=new_state)
+ state, message_id = new_state, filled
+ # else: still unavailable — fall through unchanged; never hand the signature to is_delivered
+
+ if (receipt.status is Status.DELIVERY_PENDING and route.protocol == "hyperlane"
+ and message_id is not None and dst.family in ("aleo", "evm")):
+ delivered = (bridge.hyperlane.is_delivered(message_id) if dst.family == "aleo"
+ else _module(bridge, "eth").is_delivered(message_id))
+ return _clear_action(receipt, status=Status.COMPLETED) if delivered else receipt
+
+ # 6. Aleo-origin Hyperlane without a message id: destination balance baseline
+ if receipt.status is Status.DELIVERY_PENDING and route.protocol == "hyperlane" and src.family == "aleo":
+ before, expected = state.get("destinationBalanceBeforeAtomic"), state.get("expectedDestinationIncreaseAtomic")
+ if not (isinstance(before, str) and before.isdigit() and isinstance(expected, str) and expected.isdigit()):
+ return receipt
+ current = _read_destination_balance(bridge, plan, resolved)
+ if current is None:
+ raise DeliveryUnknownError(
+ f"No destination balance reader is configured for {dst.id}: bind the {dst.id} connection "
+ "whose address is the recipient, or confirm delivery out of band")
+ if current < int(before) + int(expected):
+ return receipt
+ return _clear_action(receipt, status=Status.COMPLETED)
+
+ # 7. Other Hyperlane states are observed elsewhere
+ if route.protocol == "hyperlane":
+ return receipt
+
+ # 8. xReserve Aleo→EVM: Circle exposes no canonical delivery query
+ if (receipt.status is Status.DELIVERY_PENDING and route.protocol == "xreserve"
+ and src.family == "aleo" and dst.family == "evm"):
+ return receipt
+
+ # 9. Everything else that is not inbound xReserve
+ if route.protocol != "xreserve" or src.family != "evm" or dst.family != "aleo":
+ raise UnsupportedRouteError("Status refresh is not implemented for this bridge route")
+
+ # 10. xReserve EVM→Aleo — destination nullifier first (invariant 6)
+ if receipt.status in (Status.ATTESTATION_PENDING, Status.DELIVERY_PENDING, Status.DESTINATION_ACTION_REQUIRED):
+ nonce = state.get("nonce")
+ if not isinstance(nonce, str):
+ payload = _hex_bytes(state.get("payload"))
+ if payload is not None:
+ from .encoding import xreserve_nonce_from_payload
+ nonce = "0x" + xreserve_nonce_from_payload(payload).hex()
+ if isinstance(nonce, str) and nonce and bridge.xreserve.is_delivered(nonce, route=route):
+ return _clear_action(receipt, status=Status.COMPLETED)
+
+ if receipt.status is Status.SOURCE_CONFIRMING:
+ return _module(bridge, "eth").source_status(plan, receipt)
+
+ if receipt.status is Status.ATTESTATION_PENDING:
+ message_hash = state.get("messageHash")
+ if _hex_bytes(message_hash, length=32) is None:
+ raise CheckpointInvalidError("xReserve receipt is missing its Circle message hash")
+ attestation = bridge.xreserve.get_attestation(message_hash, route=route)
+ if attestation is None:
+ return receipt
+ with_att = {**state, "attestation": "0x" + attestation.attestation.hex()}
+ if plan.mint_mode != "private":
+ return receipt.replace(status=Status.DELIVERY_PENDING, protocol_state=with_att)
+ return receipt.replace(status=Status.DESTINATION_ACTION_REQUIRED, protocol_state=with_att,
+ next_action={"kind": "xreserve-private-mint", "chainId": dst.id})
+
+ if receipt.status is Status.DESTINATION_ACTION_REQUIRED:
+ return receipt
+
+ if receipt.status is Status.DESTINATION_CONFIRMING:
+ if not receipt.destination_tx_id:
+ raise CheckpointInvalidError("xReserve receipt is missing its Aleo destination transaction id")
+ verdict, error = aleo_transaction_status(bridge, receipt.destination_tx_id)
+ if verdict == "accepted":
+ return _clear_action(receipt, status=Status.COMPLETED)
+ if verdict == "rejected":
+ return _clear_action(receipt, status=Status.FAILED,
+ protocol_state={**state, "destinationError": error})
+ return receipt
+
+ return receipt
+
+
+# ── wait ──────────────────────────────────────────────────────────────────────
+
+def _track(bridge, plan: Plan, previous: Receipt, current: Receipt) -> None:
+ """Mirror an observed status change into the bound store (no caller callback)."""
+ if getattr(bridge, "checkpoints", None) is None:
+ return
+ _persist(bridge, create_checkpoint(plan, current, bridge.registry), current, previous_id=previous.id)
+
+
+def _status_set(values) -> set[Status]:
+ try:
+ return {v if isinstance(v, Status) else Status(v) for v in values}
+ except ValueError as exc:
+ raise ConfigurationError(f"wait(until=...) contains an unknown status: {exc}") from exc
+
+
+_TRANSIENT_BRIDGE_ERROR_RE = re.compile(r"HTTP status (429|5\d\d)|request failed:")
+
+
+def _is_transient_error(exc: Exception) -> bool:
+ """``wait``'s retry classifier: a network/RPC hiccup vs. a real problem that must propagate.
+
+ Transient: ``requests.RequestException`` (any ``requests``-based transport), ``aleo.facade.
+ errors.AleoNetworkError`` (the Aleo facade), a ``BridgeError`` whose message matches an HTTP
+ 429/5xx or a wrapped "request failed:" transport error (``SolanaRpcClient``), or a web3
+ provider/connection error. Everything else — including programming errors, ``BridgeError``s
+ about an actual on-chain failure, ``CheckpointInvalidError``, ``RouteUnavailableError`` —
+ propagates immediately.
+ """
+ try:
+ import requests
+ if isinstance(exc, requests.RequestException):
+ return True
+ except ImportError:
+ pass
+ try:
+ from aleo.facade.errors import AleoNetworkError
+ if isinstance(exc, AleoNetworkError):
+ return True
+ except ImportError:
+ pass
+ try:
+ from web3.exceptions import ProviderConnectionError
+ if isinstance(exc, ProviderConnectionError):
+ return True
+ except ImportError:
+ if type(exc).__name__ == "ProviderConnectionError": # web3 extra not installed here
+ return True
+ if isinstance(exc, BridgeError) and _TRANSIENT_BRIDGE_ERROR_RE.search(str(exc)):
+ return True
+ return False
+
+
+def wait(bridge, progress: Progress, *, until=None, poll_seconds: float = 15.0,
+ timeout_seconds: float = 1200.0, on_update: Callable[[Progress], Any] | None = None,
+ on_error: Callable[[Exception], Any] | None = None, max_consecutive_errors: int = 5) -> Progress:
+ """Poll ``get_status`` until the transfer needs the caller or finishes.
+
+ Always stops at the caller boundaries — ``SOURCE_SUBMISSION_PENDING`` (→ ``resume``),
+ ``DESTINATION_ACTION_REQUIRED`` (→ ``complete``), ``COMPLETED``, ``FAILED``, ``EXPIRED`` — plus
+ any statuses in ``until`` (a ``Status`` or its name; ``until=[]`` is a ``ConfigurationError``,
+ an unknown name too). Returns immediately when ``progress.next != "wait"`` or the receipt is
+ already at a stop. ``on_update`` fires only when the receipt changed, never on a retry.
+ ``poll_seconds`` is floored at 0.1 unless exactly 0; negative ``poll_seconds``/``timeout_seconds``
+ is a ``ConfigurationError``.
+
+ A ``get_status`` call that raises a transient error (flaky RPC/HTTP transport — see
+ :func:`_is_transient_error`) is retried with the normal poll interval, up to
+ ``max_consecutive_errors`` (default 5) consecutive failures before the last one is re-raised;
+ ``on_error`` fires on each tolerated retry so callers can log them. A non-transient error
+ propagates immediately, on the first attempt.
+
+ Hitting ``timeout_seconds`` raises ``PollingTimeoutError`` carrying the last ``status`` and
+ ``progress`` — a timeout is NOT a failure (invariant 5): the transfer is still in flight; call
+ ``wait`` again or ``recover`` later.
+ """
+ if until is not None and len(until) == 0:
+ raise ConfigurationError("wait(until=[]) has nothing to stop at: pass at least one Status or omit until")
+ plan, receipt = progress.plan, progress.receipt
+ resolve_route(bridge.registry, plan)
+ _check_receipt(plan, receipt)
+ stops = set(CALLER_BOUNDARIES) | _status_set(until or ())
+ current = to_progress(plan, receipt)
+ if current.next != "wait" or receipt.status in stops:
+ return current
+ if poll_seconds < 0 or timeout_seconds < 0:
+ raise ConfigurationError("poll_seconds and timeout_seconds must be non-negative")
+ interval = 0.0 if poll_seconds == 0 else max(0.1, float(poll_seconds))
+ deadline = time.monotonic() + timeout_seconds
+ updated = receipt
+ consecutive_errors = 0
+ while True:
+ try:
+ nxt = get_status(bridge, plan, updated)
+ except Exception as exc:
+ if not _is_transient_error(exc):
+ raise
+ consecutive_errors += 1
+ if consecutive_errors > max_consecutive_errors:
+ raise
+ if on_error is not None:
+ on_error(exc)
+ if time.monotonic() >= deadline:
+ raise PollingTimeoutError(
+ f"Bridge status polling timed out in state {updated.status.value}; the transfer is "
+ "still in flight — call wait() again or recover() from the last checkpoint. This is "
+ "not a failure.", status=updated.status, progress=to_progress(plan, updated)) from exc
+ time.sleep(interval)
+ continue
+ consecutive_errors = 0
+ if nxt != updated:
+ _track(bridge, plan, updated, nxt)
+ if on_update is not None:
+ on_update(to_progress(plan, nxt))
+ updated = nxt
+ if updated.status in stops:
+ return to_progress(plan, updated)
+ if time.monotonic() >= deadline:
+ raise PollingTimeoutError(
+ f"Bridge status polling timed out in state {updated.status.value}; the transfer is still "
+ "in flight — call wait() again or recover() from the last checkpoint. This is not a failure.",
+ status=updated.status, progress=to_progress(plan, updated))
+ time.sleep(interval)
+
+
+__all__ = ["MINT_MODES", "ResolvedRoute", "aleo_transaction_status", "complete", "execute", "get_status",
+ "is_duplicate_broadcast_error", "prepare", "progress_from_checkpoint", "quote", "recover",
+ "resolve_route", "resume", "submit_serialized", "wait"]
+
+
+# ── recover ───────────────────────────────────────────────────────────────────
+
+def _coerce_checkpoint(value):
+ if isinstance(value, Checkpoint):
+ return value
+ if isinstance(value, str):
+ return Checkpoint.from_json(value)
+ if isinstance(value, dict):
+ return Checkpoint.from_dict(value)
+ raise CheckpointInvalidError(f"recover() takes a Checkpoint, its dict, or its JSON; got {type(value).__name__}")
+
+
+def _plan_from_intent(registry: Registry, intent: dict[str, Any]) -> Plan:
+ """Rebuild the ``Plan`` behind a checkpoint by re-running ``prepare`` on its intent.
+
+ ``prepare`` is only ever a thin validating wrapper around ``_plan.build_plan``
+ (proven field-identical for every active route — see
+ ``tests/test_prepare.py::test_prepare_equals_build_plan_for_every_active_route``), so a
+ recovered plan is exactly what the original ``prepare()``/``quote()`` call produced and
+ passes ``EthModule``/``SolModule``'s field-by-field ``plan=`` checks.
+ """
+ try:
+ return prepare(registry,
+ source=(intent["source"]["chain"], intent["source"]["asset"]),
+ destination=(intent["destination"]["chain"], intent["destination"]["asset"]),
+ amount=intent["amount"], recipient=intent["recipient"], sender=intent.get("sender"),
+ protocol=intent.get("bridgeProtocol"), mint_mode=intent.get("mintMode", "public"))
+ except (KeyError, TypeError) as exc:
+ raise CheckpointInvalidError(f"Bridge checkpoint intent is incomplete: missing {exc}") from exc
+
+
+def _assert_prepared_id(serialized: Any, expected_id: str, what: str = "prepared Aleo transaction") -> str:
+ """The serialized transaction's ``id`` must equal the saved id — never substitute bytes."""
+ if not isinstance(serialized, str) or not serialized:
+ raise CheckpointInvalidError(f"Bridge checkpoint contains an invalid {what} (empty)")
+ try:
+ decoded = json.loads(serialized)
+ except json.JSONDecodeError as exc:
+ raise CheckpointInvalidError(f"Bridge checkpoint contains an invalid {what}: not JSON") from exc
+ tx_id = decoded.get("id") if isinstance(decoded, dict) else None
+ if not isinstance(tx_id, str) or tx_id != expected_id:
+ raise CheckpointInvalidError(f"Bridge checkpoint {what} id does not match its payload "
+ f"({tx_id!r} != {expected_id!r})")
+ return tx_id
+
+
+def _finish(bridge, plan: Plan, receipt: Receipt, checkpoint_id: str) -> Progress:
+ """Reduce *receipt* to ``Progress``, dropping the checkpoint (keyed on ``checkpoint_id`` —
+ the checkpoint's OWN id, never ``receipt.id``: for Solana and EVM Hyperlane the checkpoint's
+ id is the source transaction id while the receipt's own id flips to the message id once
+ confirmed) from the bound store once the transfer reaches a terminal status.
+ """
+ store = getattr(bridge, "checkpoints", None)
+ if store is not None and receipt.status in TERMINAL:
+ store.delete(checkpoint_id)
+ return to_progress(plan, receipt)
+
+
+def _verification_from_delivery(dv: dict[str, Any]) -> dict[str, str]:
+ """Validate and translate a checkpoint's ``deliveryVerification`` block (Aleo-origin Hyperlane
+ only); ``{}`` when absent. Raises :class:`CheckpointInvalidError` on a malformed block (missing
+ key or non-digit value) rather than ``KeyError`` — the block came from a stored file, not a
+ live read, so it is untrusted input."""
+ if not dv:
+ return {}
+ before, expected = dv.get("balanceBeforeAtomic"), dv.get("expectedIncreaseAtomic")
+ if not (isinstance(before, str) and before.isdigit() and isinstance(expected, str) and expected.isdigit()):
+ raise CheckpointInvalidError("Bridge checkpoint contains invalid destination balance verification state")
+ return {"destinationBalanceBeforeAtomic": before, "expectedDestinationIncreaseAtomic": expected}
+
+
+def _reconstruct_source_receipt(plan: Plan, resolved: ResolvedRoute, cp: Checkpoint,
+ verification: dict[str, str]) -> Receipt:
+ """Build the pre-refresh source ``Receipt`` purely from a checkpoint's stored fields — no
+ network, no signing. Shared by ``recover`` (which then reads live chain state to refine
+ non-``SOURCE_SUBMISSION_PENDING`` results) and the fully offline ``progress_from_checkpoint``
+ (which stops here), so the two can never drift apart on what a checkpoint alone can tell you.
+
+ Mirrors ``create_checkpoint``'s own allowlist: a ``preparedTransaction`` with no
+ ``transactionId`` is an unbroadcast Aleo leg (``SOURCE_SUBMISSION_PENDING`` — next: resume);
+ a ``transactionId`` alone (any chain family) is a submitted, still-confirming leg
+ (``SOURCE_CONFIRMING``); an EVM leg with only approvals and no ``transactionId`` yet is
+ waiting on its deposit/dispatch (``SOURCE_SUBMISSION_PENDING`` too — the same "call resume()"
+ signal; ``resume()`` re-verifies against live chain history before repeating anything, so an
+ offline guess here is never unsafe); anything else has nothing to build a receipt from.
+ """
+ src = resolved.source_chain
+ source = cp.source or {}
+ approvals = [a for a in (source.get("approvalTransactionIds") or []) if isinstance(a, str)]
+
+ if src.family == "aleo":
+ prepared = source.get("preparedTransaction")
+ if prepared and not source.get("transactionId"):
+ if cp.destination or approvals:
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains transactions that are invalid for a prepared Aleo source route")
+ tx_id = _assert_prepared_id(prepared.get("serializedTransaction"), str(prepared.get("transactionId")))
+ return Receipt(id=tx_id, protocol=plan.protocol, status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "preparedTransaction": prepared["serializedTransaction"],
+ **verification})
+ tx_id = source.get("transactionId")
+ if not tx_id:
+ raise CheckpointInvalidError("Bridge checkpoint contains no submitted source transaction")
+ if cp.destination or approvals:
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains transactions that are invalid for an Aleo source route")
+ return Receipt(id=tx_id, protocol=plan.protocol, status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id,
+ protocol_state={"routeId": plan.route_id, **verification})
+
+ if src.family == "solana":
+ tx_id = source.get("transactionId")
+ if not tx_id:
+ raise CheckpointInvalidError("Bridge checkpoint contains no submitted source transaction")
+ if cp.destination or approvals:
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains transactions that are invalid for a Solana source route")
+ blockhash, last_valid = source.get("blockhash"), source.get("lastValidBlockHeight")
+ if (blockhash is not None or last_valid is not None) and (
+ not isinstance(blockhash, str) or not blockhash
+ or not isinstance(last_valid, str) or not last_valid.isdigit()):
+ raise CheckpointInvalidError("Bridge checkpoint contains an invalid Solana blockhash lifetime")
+ state: dict[str, Any] = {"routeId": plan.route_id}
+ if isinstance(blockhash, str) and isinstance(last_valid, str):
+ state.update(blockhash=blockhash, lastValidBlockHeight=last_valid)
+ return Receipt(id=tx_id, protocol=plan.protocol, status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id,
+ protocol_state=state)
+
+ if src.family == "evm":
+ tx_id = source.get("transactionId")
+ if tx_id:
+ state: dict[str, Any] = {"routeId": plan.route_id}
+ if approvals:
+ state["approvalTxIds"] = approvals
+ return Receipt(id=tx_id, protocol=plan.protocol, status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id,
+ protocol_state=state)
+ if approvals:
+ return Receipt(id=approvals[-1], protocol=plan.protocol, status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": approvals})
+ raise CheckpointInvalidError("Bridge checkpoint contains no submitted source transaction")
+
+ raise UnsupportedRouteError(f"Bridge checkpoint recovery is not implemented for source family {src.family!r}")
+
+
+def _apply_destination_overlay(resolved: ResolvedRoute, cp: Checkpoint, receipt: Receipt) -> Receipt:
+ """Offline-only: fold a checkpoint's own destination fields (inbound xReserve) into *receipt*
+ without any network read — a submitted destination transaction id becomes
+ ``DESTINATION_CONFIRMING``, an unbroadcast prepared one becomes ``DESTINATION_ACTION_REQUIRED``
+ (ready for ``complete()``). Never validates either against live chain state the way ``recover``
+ does over the wire (that would need the Circle attestation, which a checkpoint never stores);
+ ``complete()``/``resume()`` re-verify before acting, so an offline-optimistic guess here is
+ never unsafe — only ever a prompt to call the verb that actually checks.
+ """
+ destination = cp.destination or {}
+ if not destination:
+ return receipt
+ if resolved.route.protocol != "xreserve" or resolved.destination_chain.family != "aleo":
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains a destination transaction that is invalid for this route")
+ prepared_dest = destination.get("preparedTransaction")
+ if prepared_dest and destination.get("transactionId"):
+ raise CheckpointInvalidError(
+ "Bridge checkpoint cannot contain both prepared and submitted destination transactions")
+ if destination.get("transactionId"):
+ receipt = receipt.replace(status=Status.DESTINATION_CONFIRMING, destination_tx_id=destination["transactionId"])
+ if prepared_dest:
+ tx_id = _assert_prepared_id(prepared_dest.get("serializedTransaction"), str(prepared_dest.get("transactionId")),
+ "prepared Aleo destination transaction")
+ receipt = receipt.replace(id=tx_id, status=Status.DESTINATION_ACTION_REQUIRED,
+ next_action={"kind": "xreserve-private-mint", "chainId": resolved.destination_chain.id},
+ protocol_state={**receipt.protocol_state,
+ "preparedDestinationTransaction": prepared_dest["serializedTransaction"]})
+ return receipt
+
+
+def progress_from_checkpoint(registry: Registry, checkpoint) -> Progress:
+ """Pure, fully offline reconstruction of a checkpoint's ``Progress`` — no network, no signing.
+
+ Used by ``Bridge.pending()`` instead of ``recover`` so that listing every in-flight transfer
+ never depends on any chain being reachable (one unreachable RPC must never hide every other
+ transfer). Rebuilds the ``Plan`` from the checkpoint's own intent and validates it against the
+ live registry exactly like ``recover`` (still raises on a bad format, version, or route
+ mismatch — those mean the record cannot be interpreted at all). From there, everything is
+ built purely from the checkpoint's stored fields (:func:`_reconstruct_source_receipt` /
+ :func:`_apply_destination_overlay`) — the same reconstruction ``recover`` performs before its
+ own live refresh. A checkpoint whose stored fields cannot be interpreted after that point
+ (e.g. no submitted or prepared source transaction at all) folds into a ``Progress`` with
+ ``next == "failed"`` and ``error`` set, instead of raising — so one malformed record can never
+ hide the others in a ``pending()`` listing.
+ """
+ cp = _coerce_checkpoint(checkpoint)
+ if cp.version != 1 or not cp.intent or not cp.route:
+ raise CheckpointInvalidError("Bridge checkpoint format is invalid or unsupported (version 1 required)")
+ plan = _plan_from_intent(registry, cp.intent)
+ if cp.route.get("registryVersion") != plan.registry_version:
+ raise RegistryVersionMismatchError(
+ f"Checkpoint was written against registry {cp.route.get('registryVersion')}; this client has "
+ f"{plan.registry_version}. Upgrade/downgrade aleo-bridge-sdk to the version that wrote it.")
+ if cp.route.get("id") != plan.route_id:
+ raise CheckpointInvalidError(
+ f"Bridge checkpoint route {cp.route.get('id')} does not match the prepared route {plan.route_id}")
+ resolved = resolve_route(registry, plan)
+ try:
+ verification = _verification_from_delivery(cp.delivery_verification or {})
+ receipt = _reconstruct_source_receipt(plan, resolved, cp, verification)
+ receipt = _apply_destination_overlay(resolved, cp, receipt)
+ except BridgeError as exc:
+ receipt = Receipt(id=cp.id, protocol=plan.protocol, status=Status.FAILED,
+ protocol_state={"routeId": plan.route_id, "sourceError": str(exc)})
+ return to_progress(plan, receipt)
+
+
+def recover(bridge, checkpoint) -> Progress:
+ """Rebuild a transfer's ``Progress`` from a saved checkpoint — reads only, never signs.
+
+ Accepts a ``Checkpoint``, its dict, or its JSON. Re-runs ``prepare`` on the
+ saved intent against the LIVE registry, then checks the route id and registry
+ version (``CheckpointInvalidError`` / ``RegistryVersionMismatchError``).
+ Aleo source: a proved-but-unbroadcast transaction yields ``next == "resume"``
+ with no network read; a submitted one gets exactly one ``get_status`` from
+ ``SOURCE_CONFIRMING``. Solana: validates the blockhash pair and reads the
+ signature status. EVM: delegates to ``bridge.eth.recover_source`` (log
+ scan); inbound xReserve additionally restores a submitted or prepared
+ private mint. The result's ``next`` tells the caller what to do.
+ """
+ cp = _coerce_checkpoint(checkpoint)
+ if cp.version != 1 or not cp.intent or not cp.route:
+ raise CheckpointInvalidError("Bridge checkpoint format is invalid or unsupported (version 1 required)")
+ plan = _plan_from_intent(bridge.registry, cp.intent)
+ if cp.route.get("registryVersion") != plan.registry_version:
+ raise RegistryVersionMismatchError(
+ f"Checkpoint was written against registry {cp.route.get('registryVersion')}; this client has "
+ f"{plan.registry_version}. Upgrade/downgrade aleo-bridge-sdk to the version that wrote it.")
+ if cp.route.get("id") != plan.route_id:
+ raise CheckpointInvalidError(
+ f"Bridge checkpoint route {cp.route.get('id')} does not match the prepared route {plan.route_id}")
+ resolved = resolve_route(bridge.registry, plan)
+ src, dst = resolved.source_chain, resolved.destination_chain
+ verification = _verification_from_delivery(cp.delivery_verification or {})
+
+ if src.family in ("aleo", "solana"):
+ receipt = _reconstruct_source_receipt(plan, resolved, cp, verification)
+ if receipt.status is Status.SOURCE_SUBMISSION_PENDING:
+ return to_progress(plan, receipt) # Aleo prepared, unbroadcast: no network read
+ receipt = get_status(bridge, plan, receipt)
+ return _finish(bridge, plan, receipt, cp.id)
+
+ if resolved.route.protocol == "hyperlane" and src.family == "evm":
+ if cp.destination:
+ raise CheckpointInvalidError(
+ "Bridge checkpoint contains a destination transaction that is invalid for this Hyperlane route")
+ receipt = _module(bridge, "eth").recover_source(plan, cp, required=False)
+ return _finish(bridge, plan, receipt, cp.id)
+
+ if resolved.route.protocol != "xreserve" or src.family != "evm" or dst.family != "aleo":
+ raise UnsupportedRouteError("Bridge checkpoint recovery is not implemented for this route")
+
+ receipt = _module(bridge, "eth").recover_source(plan, cp, required=False)
+ destination = cp.destination or {}
+ prepared_dest = destination.get("preparedTransaction")
+ if prepared_dest and destination.get("transactionId"):
+ raise CheckpointInvalidError(
+ "Bridge checkpoint cannot contain both prepared and submitted destination transactions")
+ if prepared_dest:
+ _assert_prepared_id(prepared_dest.get("serializedTransaction"), str(prepared_dest.get("transactionId")),
+ "prepared Aleo destination transaction")
+ if destination.get("transactionId"):
+ receipt = receipt.replace(status=Status.DESTINATION_CONFIRMING, destination_tx_id=destination["transactionId"])
+ if receipt.status in (Status.ATTESTATION_PENDING, Status.DESTINATION_CONFIRMING):
+ receipt = get_status(bridge, plan, receipt)
+ if prepared_dest:
+ if receipt.status is not Status.DESTINATION_ACTION_REQUIRED:
+ raise CheckpointInvalidError(
+ "Prepared destination transaction is no longer valid for the recovered bridge state")
+ receipt = receipt.replace(id=str(prepared_dest["transactionId"]),
+ protocol_state={**receipt.protocol_state,
+ "preparedDestinationTransaction": prepared_dest["serializedTransaction"]})
+ return _finish(bridge, plan, receipt, cp.id)
+
+
+# ── Idempotent Aleo rebroadcast (invariant 3) ─────────────────────────────────
+
+#: Plan 1's duplicate-broadcast classifier, re-exported under the lifecycle's own name.
+#:
+#: It is deliberately NOT reimplemented here: ``AleoCall.submit_prepared`` already applies exactly
+#: this rule to every prepared broadcast, and two rules that could ever disagree about "is this a
+#: duplicate?" is one rule too many for a funds-critical path. Only the node's *"already exists"*
+#: answer (ledger or mempool) means "this exact transaction is already known"; ``duplicate serial
+#: number`` / ``duplicate output id`` mean a DIFFERENT transaction collided with this one's records
+#: and must surface as failures — and they do, because they never say "already exists".
+is_duplicate_broadcast_error = is_duplicate_submission
+
+
+def submit_serialized(bridge, serialized: str, expected_id: str) -> str:
+ """Broadcast an already-proved transaction; a duplicate answer is success, not a failure.
+
+ Returns the transaction id the node acknowledged, which must be *expected_id* — the id of the
+ exact bytes that were broadcast. A node that answers with a different id has accepted something
+ this transfer never checkpointed, so it is refused rather than recorded as its transaction.
+ """
+ try:
+ submitted = str(bridge.aleo.network.submit_transaction(serialized)).strip().strip('"')
+ except Exception as exc: # noqa: BLE001 — the node's error type varies by transport
+ if is_duplicate_broadcast_error(exc):
+ return expected_id # the earlier broadcast won the race: nothing left to do
+ raise
+ if submitted != expected_id:
+ raise CheckpointInvalidError(
+ f"Aleo node acknowledged transaction {submitted}; expected {expected_id}. The prepared "
+ "bytes and the node's answer disagree — do not resend; inspect both ids first.")
+ return submitted
+
+
+# ── resume ────────────────────────────────────────────────────────────────────
+
+def resume(bridge, progress: Progress, *, on_checkpoint: Callable | None = None,
+ secret_nonce: str | None = None, poll_seconds: float = 1.0, timeout_seconds: float = 120.0,
+ proving: str = "delegate") -> Progress:
+ """Finish the source leg an interruption left unsubmitted — never repeats an irreversible step.
+
+ Requires ``progress.next == "resume"`` (status ``SOURCE_SUBMISSION_PENDING``); anything else is
+ a :class:`~aleo_bridge.errors.NotResumableError` pointing at ``wait``/``recover``. In particular
+ a ``SOURCE_APPROVAL_PENDING`` receipt is NOT resumable directly: call ``recover`` first, which
+ observes the approval and yields the ``SOURCE_SUBMISSION_PENDING`` progress this verb takes.
+
+ Aleo source: rebroadcasts the checkpointed transaction byte-for-byte, after checking that the
+ serialized payload's own id matches the saved one — a duplicate-transaction answer means the
+ first broadcast won the race and counts as success. The bytes are then dropped from the
+ receipt. Nothing is re-proved, so the transfer can only ever exist once on chain.
+
+ EVM source: re-scans source history from the confirmed approval
+ (``bridge.eth.recover_source(plan, checkpoint, required=True)``) and, only when that scan proves
+ no deposit/dispatch exists yet, re-quotes and authorizes the single remaining transaction
+ through the module's own ``plan=`` surface. Two guards ported from veil refuse rather than
+ guess: the re-quoted hook data must equal the hook the checkpointed approval committed to (so a
+ private mint can never be re-hooked to a commitment its recipient cannot open), and the
+ allowance must still cover the deposit (a vanished allowance means something else spent it, and
+ re-approving is a second irreversible step ``resume`` does not own). A confirmed approval is
+ never repeated; the ids already recorded are carried into the new receipt.
+
+ Solana source: ``SolCall`` has no approval step and no pre-broadcast state to continue, so there
+ is nothing to resume — ``recover``/``wait`` observe the signature instead.
+
+ ``secret_nonce`` is mandatory when ``plan.mint_mode == "private"``, and is checked before any
+ RPC: the SDK never stored it, and a silent ``"0scalar"`` fallback would commit the deposit to a
+ hook nobody can open. ``proving`` is accepted for symmetry with ``execute``/``complete`` and is
+ never used — no resume path ever proves anything: an Aleo leg rebroadcasts bytes that were
+ already proved, and an EVM leg has no proofs at all.
+ """
+ plan, receipt = progress.plan, progress.receipt
+ if progress.next != "resume" or receipt.status is not Status.SOURCE_SUBMISSION_PENDING:
+ raise NotResumableError(
+ "Bridge progress has no source submission to resume (next must be 'resume' at "
+ "SOURCE_SUBMISSION_PENDING); call wait() or recover() to refresh it")
+ resolved = resolve_route(bridge.registry, plan)
+ _require_active(resolved.route)
+ _check_receipt(plan, receipt)
+ emit = _Emitter(bridge, plan, on_checkpoint)
+ state = receipt.protocol_state
+ family = resolved.source_chain.family
+
+ if family == "aleo":
+ serialized = state.get("preparedTransaction")
+ if not isinstance(serialized, str) or not serialized:
+ raise NotResumableError(
+ "Prepared Aleo transfer is missing its serialized transaction: resume() rebroadcasts "
+ "the exact proved bytes and never re-proves. Recover from the checkpoint written "
+ "between proving and broadcast, or start the transfer over if none exists.")
+ tx_id = _assert_prepared_id(serialized, receipt.id)
+ submit_serialized(bridge, serialized, tx_id)
+ new_state: dict[str, Any] = {"routeId": plan.route_id}
+ for key in ("destinationBalanceBeforeAtomic", "expectedDestinationIncreaseAtomic"):
+ if isinstance(state.get(key), str):
+ new_state[key] = state[key]
+ submitted = Receipt(id=tx_id, protocol=plan.protocol, status=Status.SOURCE_CONFIRMING,
+ source_tx_id=tx_id, protocol_state=new_state)
+ emit(submitted)
+ emit.finalize()
+ return to_progress(plan, submitted)
+
+ if family == "solana":
+ raise NotResumableError(
+ "Solana source legs have no resumable state: the transfer is signed and broadcast in one "
+ "step, so nothing is ever left to submit. Call recover() or wait() to observe the "
+ "signature instead.")
+
+ if family != "evm":
+ raise UnsupportedRouteError(f"Source resumption is not implemented for {resolved.source_chain.id}")
+
+ eth = _module(bridge, "eth")
+ _assert_sender(plan, bridge.ethereum.address, family="evm")
+ is_xreserve = resolved.route.protocol == "xreserve"
+ nonce = _mint_secret(plan, secret_nonce) if is_xreserve else None # before any RPC
+ saved_hook = state.get("hookData")
+ if is_xreserve and _hex_bytes(saved_hook, length=HOOK_DATA_BYTES) is None:
+ # Without the hook the approval committed to there is nothing to compare the re-quote
+ # against, so the guard below would silently pass and the deposit could be re-hooked to a
+ # different commitment. Refuse here, before any RPC, rather than resume half-blind.
+ raise NotResumableError(
+ "This transfer's checkpoint carries no xReserve hook data (a 65-byte 0x hex string); "
+ "recover() and re-quote instead of resuming — resume() will not re-derive the hook the "
+ "approval committed to")
+
+ recovered = eth.recover_source(plan, create_checkpoint(plan, receipt, bridge.registry), required=True)
+ if recovered.status is not Status.SOURCE_SUBMISSION_PENDING:
+ emit(recovered) # history already holds the irreversible step
+ emit.finalize()
+ return to_progress(plan, recovered)
+
+ if is_xreserve:
+ quoted = eth.quote_deposit_usdc(plan=plan, secret_nonce=nonce)
+ if saved_hook.lower() != ("0x" + quoted.hook_data.hex()).lower(): # always runs: validated above
+ raise NotResumableError(
+ "The re-quoted hook data does not match the hook this transfer's approval committed "
+ "to: the secret nonce differs from the one used at execute(). Pass that same "
+ "secret_nonce — depositing under another hook mints to a commitment the recipient "
+ "can never open.")
+ else:
+ quoted = eth.quote_transfer_remote(plan=plan)
+ if quoted.approval_required:
+ raise NotResumableError(
+ "The approval recorded for this transfer no longer covers it: its allowance is gone. "
+ "Inspect Ethereum source history before starting another transfer — resume() will not "
+ "issue a second approval.")
+
+ call = eth.deposit_usdc(plan=plan, secret_nonce=nonce) if is_xreserve else eth.transfer_remote(plan=plan)
+ result = call.send(wait=True, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds,
+ on_checkpoint=emit)
+ submitted = result.receipt
+ prior = [a for a in (state.get("approvalTxIds") or []) if isinstance(a, str)]
+ approvals = prior + [a for a in (submitted.protocol_state.get("approvalTxIds") or []) if a not in prior]
+ if approvals != list(submitted.protocol_state.get("approvalTxIds") or []):
+ submitted = submitted.replace(protocol_state={**submitted.protocol_state, "approvalTxIds": approvals})
+ emit(submitted)
+ emit.finalize()
+ return to_progress(plan, submitted)
+
+
+# ── complete ──────────────────────────────────────────────────────────────────
+
+def complete(bridge, progress: Progress, *, secret_nonce: str | None = None,
+ on_checkpoint: Callable | None = None, proving: str = "delegate") -> Progress:
+ """Submit the one user-signed Aleo transaction a private USDCx mint needs.
+
+ Requires ``progress.next == "complete"`` — Circle has attested the deposit and the receipt
+ carries ``next_action == {"kind": "xreserve-private-mint", "chainId": }``. The
+ persisted payload (305 bytes), message hash (32 bytes) and attestation hex are re-validated
+ first, then one of two paths runs:
+
+ * a ``preparedDestinationTransaction`` left by an earlier interrupted attempt is rebroadcast
+ byte-for-byte (a duplicate answer is success, and no ``secret_nonce`` is needed — those bytes
+ are already proved), or
+ * ``bridge.xreserve.private_mint`` builds the mint — re-verifying on the way that
+ ``(recipient, secret_nonce)`` really opens the attested hook-data commitment, so a wrong nonce
+ never reaches proving — which is then proved, checkpointed BEFORE broadcast, and broadcast.
+
+ The source deposit is never repeated, and the secret nonce, the attestation and the hook data
+ are never written to a receipt, a checkpoint or a ``Progress``. ``secret_nonce`` must be the
+ value used at ``execute``; it is required for a private plan on the proving path
+ (``ConfigurationError``, raised before any RPC).
+ """
+ if progress.next != "complete":
+ raise NotResumableError(
+ "Bridge progress has no destination action to complete (next must be 'complete'); call "
+ "wait() to refresh it — the Circle attestation may still be pending")
+ plan, receipt = progress.plan, progress.receipt
+ # Deliberately no _require_active here (unlike resume): by the time a transfer reaches
+ # DESTINATION_ACTION_REQUIRED the USDC is already deposited on Ethereum, and refusing the mint
+ # because the registry has since parked the route would strand it. The proving path still hits
+ # XReserveModule's own availability check; a rebroadcast of already-proved bytes needs none.
+ resolved = resolve_route(bridge.registry, plan)
+ _check_receipt(plan, receipt)
+ action = receipt.next_action or {}
+ if (receipt.status is not Status.DESTINATION_ACTION_REQUIRED
+ or action.get("kind") != "xreserve-private-mint"
+ or action.get("chainId") != resolved.destination_chain.id):
+ raise NotResumableError(
+ "Bridge receipt carries no supported destination action: complete() only finishes an "
+ "xReserve private mint, on this transfer's own destination chain")
+ if (resolved.route.protocol != "xreserve" or resolved.source_chain.family != "evm"
+ or resolved.destination_chain.family != "aleo"):
+ raise UnsupportedRouteError("Destination completion is not implemented for this bridge route")
+
+ state = receipt.protocol_state
+ payload = _hex_bytes(state.get("payload"), length=305)
+ message_hash = _hex_bytes(state.get("messageHash"), length=32)
+ attestation = _hex_bytes(state.get("attestation"))
+ if payload is None or message_hash is None or not attestation:
+ raise AttestationError(
+ "Ready xReserve receipt is missing its validated Circle attestation (a 305-byte payload, "
+ "a 32-byte messageHash and the attestation hex); refresh it with wait()")
+ emit = _Emitter(bridge, plan, on_checkpoint)
+
+ prepared_dest = state.get("preparedDestinationTransaction")
+ if prepared_dest is not None:
+ tx_id = _assert_prepared_id(prepared_dest, receipt.id, "prepared Aleo destination transaction")
+ submit_serialized(bridge, prepared_dest, tx_id)
+ submitted = receipt.replace(
+ status=Status.DESTINATION_CONFIRMING, destination_tx_id=tx_id, next_action=None,
+ protocol_state={k: v for k, v in state.items() if k != "preparedDestinationTransaction"})
+ emit(submitted)
+ emit.finalize()
+ return to_progress(plan, submitted)
+
+ nonce = _mint_secret(plan, secret_nonce) # before any RPC
+ att = Attestation(payload=payload, message_hash=message_hash, attestation=attestation, status="complete")
+ call = bridge.xreserve.private_mint(att, plan.recipient, secret_nonce=nonce, route=resolved.route)
+ prepared = _prepare_aleo(call, proving)
+ # invariant 3: the exact bytes live in a checkpoint before the network can ever see them
+ emit(receipt.replace(id=prepared.transaction_id,
+ protocol_state={**state, "preparedDestinationTransaction": prepared.serialized}))
+ call.submit_prepared(prepared, wait=False) # polling is wait()'s job, not complete()'s
+ submitted = receipt.replace(
+ status=Status.DESTINATION_CONFIRMING, destination_tx_id=prepared.transaction_id, next_action=None,
+ protocol_state={**{k: v for k, v in state.items() if k != "preparedDestinationTransaction"},
+ "destinationProgram": call.program_id, "destinationFunction": call.function_name})
+ emit(submitted)
+ emit.finalize()
+ return to_progress(plan, submitted)
diff --git a/bridge-sdk/python/aleo_bridge/mcp.py b/bridge-sdk/python/aleo_bridge/mcp.py
new file mode 100644
index 0000000..3afaf1e
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/mcp.py
@@ -0,0 +1,132 @@
+"""MCP server exposing the bridge lifecycle as tools (the ``[mcp]`` extra).
+
+Run: ``python -m aleo_bridge.mcp``
+
+Uses the low-level ``mcp.server.Server`` (not FastMCP) so each tool advertises the exact JSON
+schema from :func:`~aleo_bridge.agent.bridge_tools`. Tools run against the synchronous
+:class:`~aleo_bridge.client.Bridge` in a worker thread. Writes stay behind ``confirm: true``
+exactly as in ``agent.py`` — the MCP transport adds no privileges of its own.
+
+Every ``mcp.*`` import here is lazy: importing this module (and ``import aleo_bridge``) never
+requires the ``mcp`` package, and the first call into it that actually needs ``mcp`` raises the
+SDK's own :class:`~aleo_bridge.errors.MissingExtraError` naming the real extra
+(``pip install 'aleo-bridge-sdk[mcp]'``) instead of a bare ``ImportError`` from deep inside this
+module.
+
+Environment (all read by ``Bridge.from_env`` — nothing here reads the environment directly, and
+nothing here logs key material):
+ BRIDGE_PRIVATE_KEY Aleo signer (required)
+ ALEO_ENDPOINT / ALEO_NETWORK / ALEO_API_KEY / ALEO_CONSUMER_ID
+ ETHEREUM_RPC_URL / EVM_PRIVATE_KEY Ethereum connection (both or neither)
+ SOLANA_RPC_URL / SOLANA_PRIVATE_KEY Solana connection
+ BRIDGE_CHECKPOINT_DIR optional FileCheckpointStore directory
+"""
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING, Any
+
+from .agent import bridge_tools, dispatch_tool
+from .errors import MissingExtraError
+
+if TYPE_CHECKING: # pragma: no cover - typing only, never imported at runtime
+ from mcp.server import Server
+ from mcp.types import TextContent, Tool
+
+_FEATURE = "the MCP server"
+
+
+def _mcp_types() -> tuple[Any, Any]:
+ """``(Tool, TextContent)``, or :class:`MissingExtraError` when the extra is not installed."""
+ try:
+ from mcp.types import TextContent, Tool
+ except ImportError as exc:
+ raise MissingExtraError("mcp", _FEATURE) from exc
+ return Tool, TextContent
+
+
+def _mcp_server_cls() -> Any:
+ """``mcp.server.Server``, or :class:`MissingExtraError` when the extra is not installed."""
+ try:
+ from mcp.server import Server
+ except ImportError as exc:
+ raise MissingExtraError("mcp", _FEATURE) from exc
+ return Server
+
+
+def tool_definitions() -> "list[Tool]":
+ """The agent tools as MCP ``Tool`` objects with their exact schemas."""
+ Tool, _TextContent = _mcp_types()
+ return [Tool(name=t["name"], description=t["description"], inputSchema=t["input_schema"])
+ for t in bridge_tools()]
+
+
+async def call_tool(bridge: Any, name: str, arguments: dict[str, Any]) -> "list[TextContent]":
+ """Execute one tool in a worker thread; result as JSON text content.
+
+ Runs the synchronous ``dispatch_tool`` off the event loop so a slow chain read never blocks
+ other in-flight MCP requests. ``dispatch_tool`` already renders every ``BridgeError`` as a
+ JSON-serializable dict and holds writes behind ``confirm: true`` — this function adds nothing
+ beyond the thread hop and the JSON encoding.
+ """
+ _Tool, TextContent = _mcp_types()
+ from anyio import to_thread
+
+ result = await to_thread.run_sync(lambda: dispatch_tool(bridge, name, arguments))
+ return [TextContent(type="text", text=json.dumps(result))]
+
+
+def build_server(bridge: Any, *, name: str = "aleo-bridge") -> "Server":
+ """An MCP server with every agent tool registered against *bridge*.
+
+ No second tool list: both handlers below delegate straight to :func:`tool_definitions` /
+ :func:`call_tool`, which read the same table :func:`~aleo_bridge.agent.bridge_tools` and
+ :func:`~aleo_bridge.agent.dispatch_tool` use everywhere else.
+ """
+ Server = _mcp_server_cls()
+ server: Any = Server(name)
+
+ @server.list_tools()
+ async def _list_tools() -> "list[Tool]":
+ return tool_definitions()
+
+ @server.call_tool()
+ async def _call_tool(tool_name: str, arguments: dict[str, Any]) -> "list[TextContent]":
+ return await call_tool(bridge, tool_name, arguments)
+
+ return server
+
+
+def serve(bridge: Any) -> None:
+ """Serve the bridge tools over stdio until the client disconnects (blocking)."""
+ try:
+ import anyio
+ from mcp.server.stdio import stdio_server
+ except ImportError as exc:
+ raise MissingExtraError("mcp", _FEATURE) from exc
+
+ server = build_server(bridge)
+
+ async def _run() -> None:
+ async with stdio_server() as (read, write):
+ await server.run(read, write, server.create_initialization_options())
+
+ anyio.run(_run)
+
+
+def main() -> None:
+ """``python -m aleo_bridge.mcp``: bind the bridge from the environment and serve.
+
+ ``Bridge.from_env()`` already reads ``BRIDGE_PRIVATE_KEY``, the EVM/Solana keys and their
+ aliases, and ``BRIDGE_CHECKPOINT_DIR`` — this function never re-reads or logs any of them.
+ """
+ from .client import Bridge
+
+ serve(Bridge.from_env())
+
+
+if __name__ == "__main__":
+ main()
+
+
+__all__ = ["tool_definitions", "call_tool", "build_server", "serve", "main"]
diff --git a/bridge-sdk/python/aleo_bridge/privacy.py b/bridge-sdk/python/aleo_bridge/privacy.py
new file mode 100644
index 0000000..d909714
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/privacy.py
@@ -0,0 +1,115 @@
+"""shield / unshield for Aleo assets with a privacy capability (port of veil actions/shield.ts, unshield.ts,
+internal/aleoPrivacy.ts) plus local record selection through ``aleo.records.find``."""
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING, Any
+
+from ._calls import AleoCall
+from .errors import ConfigurationError, InsufficientBalanceError, InvalidAmountError, InvalidRecipientError, UnsupportedRouteError
+from .registry import Asset
+from .types import PrivacyReceipt
+from .units import format_decimal_amount, resolve_amount
+
+if TYPE_CHECKING: # pragma: no cover
+ from .client import Bridge
+
+_AMOUNT_RE = re.compile(r"\bamount:\s*(\d+)u128")
+
+
+def record_amount(plaintext: str) -> int | None:
+ """The ``amount`` member of a Token record plaintext, or None when absent."""
+ match = _AMOUNT_RE.search(plaintext or "")
+ return int(match.group(1)) if match else None
+
+
+class PrivacyModule:
+ """``bridge.privacy`` — convert between public balances and private records."""
+
+ def __init__(self, bridge: "Bridge") -> None:
+ self._bridge = bridge
+
+ def _asset(self, asset: Any, operation: str) -> Asset:
+ resolved = self._bridge.registry.asset(asset)
+ chain = self._bridge.registry.chain(resolved.chain_id)
+ if chain.family != "aleo" or resolved.privacy is None:
+ raise UnsupportedRouteError(f'Bridge asset "{resolved.id}" does not support {operation}')
+ return resolved
+
+ def _amount(self, asset: Asset, amount: Any, amount_atomic: int | None, operation: str) -> tuple[int, str]:
+ atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=asset.decimals)
+ if atomic <= 0:
+ raise InvalidAmountError(f"{operation} amount must be greater than zero")
+ return atomic, f"{atomic}u128"
+
+ def _recipient(self, asset: Asset, recipient: str | None) -> str:
+ value = recipient if recipient is not None else self._bridge.aleo_address()
+ if not asset.matches_address(value):
+ raise InvalidRecipientError(f"Recipient does not match the Aleo address format: {value}")
+ return value
+
+ def select_record(self, program: str, amount_atomic: int, account: Any = None) -> str:
+ """Smallest unspent ``program``/``Token`` record covering *amount_atomic* (plaintext string)."""
+ rows = self._bridge.aleo.records.find(account, program=program, record="Token")
+ amounts: list[tuple[int, str]] = []
+ for row in rows:
+ plaintext = row.get("record_plaintext") if isinstance(row, dict) else getattr(row, "record_plaintext", None)
+ value = record_amount(plaintext) if plaintext else None
+ if value is not None:
+ amounts.append((value, plaintext))
+ covering = [entry for entry in amounts if entry[0] >= amount_atomic]
+ if not covering:
+ largest = max((value for value, _ in amounts), default=0)
+ raise InsufficientBalanceError(
+ f"No unspent {program} Token record covers {amount_atomic}; largest available is {largest}. "
+ "Shield more, join records, or lower the amount.")
+ return min(covering)[1]
+
+ def shield(self, asset: Any, *, amount: Any = None, amount_atomic: int | None = None,
+ recipient: str | None = None) -> AleoCall[PrivacyReceipt]:
+ """Public balance → private record. ARC-22 names the private recipient; ARC-20 always credits the caller."""
+ resolved = self._asset(asset, "shielding")
+ atomic, literal = self._amount(resolved, amount, amount_atomic, "Shielding")
+ privacy = resolved.privacy
+ assert privacy is not None
+ if privacy.kind == "arc22":
+ function, inputs = "transfer_public_to_private", [self._recipient(resolved, recipient), literal]
+ else:
+ if recipient is not None and recipient != self._bridge.aleo_address():
+ raise ConfigurationError("ARC-20 shield always credits the caller; omit recipient=")
+ function, inputs = "shield", [literal]
+ human = format_decimal_amount(atomic, resolved.decimals)
+
+ def build(tx_id: str, _outputs: list[str]) -> PrivacyReceipt:
+ return PrivacyReceipt(tx_id, resolved.id, human, atomic, "shield")
+
+ return self._bridge._call(privacy.program, function, inputs, build)
+
+ def unshield(self, asset: Any, *, amount: Any = None, amount_atomic: int | None = None, record: str | None = None,
+ merkle_proof: str | None = None, recipient: str | None = None) -> AleoCall[PrivacyReceipt]:
+ """Private record → public balance. The record defaults to the smallest covering one; ARC-22 also needs the
+ freeze-list exclusion proof (computed for the signer; veil's empty pair when the list is empty)."""
+ resolved = self._asset(asset, "unshielding")
+ atomic, literal = self._amount(resolved, amount, amount_atomic, "Unshielding")
+ privacy = resolved.privacy
+ assert privacy is not None
+ record = record if record is not None else self.select_record(privacy.program, atomic)
+ if privacy.kind == "arc22":
+ proof = merkle_proof if merkle_proof is not None else \
+ self._bridge.freezelist.exclusion_proof(self._bridge.aleo_address(), privacy.program)
+ function, inputs = "transfer_private_to_public", [self._recipient(resolved, recipient), literal, record, proof]
+ else:
+ if recipient is not None and recipient != self._bridge.aleo_address():
+ raise ConfigurationError("ARC-20 unshield always credits the caller; omit recipient=")
+ if merkle_proof is not None:
+ raise ConfigurationError("ARC-20 unshield takes no Merkle proof; omit merkle_proof=")
+ function, inputs = "unshield", [record, literal]
+ human = format_decimal_amount(atomic, resolved.decimals)
+
+ def build(tx_id: str, _outputs: list[str]) -> PrivacyReceipt:
+ return PrivacyReceipt(tx_id, resolved.id, human, atomic, "unshield")
+
+ return self._bridge._call(privacy.program, function, inputs, build)
+
+
+__all__ = ["PrivacyModule", "record_amount"]
diff --git a/bridge-sdk/python/aleo_bridge/profile.py b/bridge-sdk/python/aleo_bridge/profile.py
new file mode 100644
index 0000000..40d749c
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/profile.py
@@ -0,0 +1,107 @@
+"""On-disk Aleo identity for the bridge — one profile per home directory (``$ALEO_BRIDGE_HOME`` or ``~/.aleo-bridge``).
+
+Holds ONLY the Aleo key (mode 600). EVM and Solana keys are never written by this package; they come from
+``EVM_PRIVATE_KEY`` / ``SOLANA_PRIVATE_KEY`` or caller-built connections. Shares no code with shield-swap.
+"""
+from __future__ import annotations
+
+import json
+import os
+from pathlib import Path
+from typing import Any
+
+from .errors import ConfigurationError
+
+DEFAULT_ENDPOINT = "https://edge.provable.com/api"
+NETWORKS = ("mainnet", "testnet")
+_PROFILE = "profile.json"
+_CHECKPOINTS = "checkpoints"
+
+
+def _initial_key(network: str) -> tuple[str, str]:
+ """(private_key, address): imported from BRIDGE_PRIVATE_KEY / BRIDGE_PRIVATE_KEY_FILE, else freshly random."""
+ import aleo
+ net = getattr(aleo, network)
+ key = (os.environ.get("BRIDGE_PRIVATE_KEY") or "").strip()
+ key_file = os.environ.get("BRIDGE_PRIVATE_KEY_FILE")
+ if not key and key_file:
+ key = Path(key_file).expanduser().read_text().strip()
+ private_key = net.PrivateKey.from_string(key) if key else net.PrivateKey.random()
+ return str(private_key), str(private_key.address)
+
+
+class Profile:
+ """A persistent Aleo identity: created on first use, reused every session after.
+
+ ``Profile.load_or_create()`` does both, so callers never branch on existence. *network* and
+ *endpoint* apply only when creating; an existing profile keeps the values it was created with.
+ """
+
+ def __init__(self, home: Path, data: dict[str, Any]) -> None:
+ self.home = home
+ self._data = data
+
+ def __repr__(self) -> str:
+ return f"Profile({self.address!r}, network={self.network!r}, home={str(self.home)!r})"
+
+ @staticmethod
+ def default_home() -> Path:
+ env = os.environ.get("ALEO_BRIDGE_HOME")
+ return Path(env).expanduser() if env else Path.home() / ".aleo-bridge"
+
+ @classmethod
+ def load_or_create(cls, home: "Path | str | None" = None, *, network: str = "mainnet",
+ endpoint: str = DEFAULT_ENDPOINT) -> "Profile":
+ if network not in NETWORKS:
+ raise ConfigurationError(f"Profile network must be one of {NETWORKS}, got {network!r}")
+ home_path = Path(home).expanduser() if home is not None else cls.default_home()
+ path = home_path / _PROFILE
+ if path.exists():
+ path.chmod(0o600) # heal a loose mode on load
+ profile = cls(home_path, json.loads(path.read_text()))
+ else:
+ if home_path.is_dir():
+ home_path.chmod(0o700) # heal a loose mode on an existing dir
+ else:
+ home_path.mkdir(parents=True, exist_ok=True, mode=0o700)
+ home_path.chmod(0o700) # mkdir's mode is subject to umask; make it exact
+ private_key, address = _initial_key(network)
+ data = {"address": address, "private_key": private_key, "network": network, "endpoint": endpoint}
+ try:
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ except FileExistsError:
+ # Lost the creation race to another process — adopt the winner's key rather than
+ # clobbering its (already in-use) profile.
+ path.chmod(0o600)
+ data = json.loads(path.read_text())
+ else:
+ with os.fdopen(fd, "w") as handle:
+ handle.write(json.dumps(data, indent=1))
+ profile = cls(home_path, data)
+ profile.checkpoint_dir.mkdir(parents=True, exist_ok=True)
+ return profile
+
+ @property
+ def address(self) -> str:
+ return self._data["address"]
+
+ @property
+ def private_key(self) -> str:
+ """Controls the account — never log it or send it to a service."""
+ return self._data["private_key"]
+
+ @property
+ def network(self) -> str:
+ return self._data["network"]
+
+ @property
+ def endpoint(self) -> str:
+ return self._data.get("endpoint", DEFAULT_ENDPOINT)
+
+ @property
+ def checkpoint_dir(self) -> Path:
+ """Directory plan 4 binds as ``FileCheckpointStore``; created with the profile."""
+ return self.home / _CHECKPOINTS
+
+
+__all__ = ["DEFAULT_ENDPOINT", "Profile"]
diff --git a/bridge-sdk/python/aleo_bridge/registry.py b/bridge-sdk/python/aleo_bridge/registry.py
new file mode 100644
index 0000000..0d8c401
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/registry.py
@@ -0,0 +1,290 @@
+"""Typed, validated views over the pinned deployment registry (``_registry_data.py``).
+
+Discovery is local: chains, assets and routes come from a reviewed snapshot, never from live lookups.
+Assets are addressed as ``"chain/key"`` or ``(chain, key)``; symbols and chain ids compare
+case-insensitively. Route ``metadata`` keeps veil's camelCase keys.
+"""
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass, field
+from types import ModuleType
+from typing import Any, Iterable, Mapping
+
+from . import _registry_data
+from .errors import AmbiguousRouteError, ConfigurationError, RouteNotFoundError
+
+AssetRef = "str | tuple[str, str] | Asset"
+FAMILIES = ("aleo", "evm", "solana")
+AVAILABILITIES = ("active", "metadata-required", "disabled")
+PRIVACY_KINDS = ("arc20", "arc22")
+_SOLANA_REQUIRED_METADATA = (
+ "warpProgramAddress", "tokenPda", "nativeCollateralPda", "dispatchAuthorityPda", "mailboxProgramAddress",
+ "mailboxOutboxPda", "igpProgramAddress", "igpProgramDataPda", "igpAccount", "splNoopProgramAddress",
+ "destinationDomain", "destinationGasAmount", "registryCommit", "solanaReviewedAt", "solanaConfigSource",
+)
+
+
+@dataclass(frozen=True)
+class Chain:
+ id: str
+ display_name: str
+ family: str # "aleo" | "evm" | "solana"
+ environment: str # "mainnet" | "testnet"
+ native_symbol: str
+ protocol_domains: Mapping[str, int] = field(default_factory=dict) # {"xreserve": 0, "hyperlane": 1}
+
+
+@dataclass(frozen=True)
+class Locator:
+ kind: str # "aleo-program" | "evm-contract" | "solana-mint" | "native"
+ value: str
+ token_id: str | None = None
+
+
+@dataclass(frozen=True)
+class Privacy:
+ kind: str # "arc20" | "arc22"
+ program: str
+
+
+@dataclass(frozen=True)
+class Asset:
+ id: str # "chain/key"
+ key: str
+ chain_id: str
+ symbol: str
+ name: str
+ decimals: int
+ kind: str # "native" | "token"
+ locator: Locator | None = None
+ address_regex: str | None = None
+ privacy: Privacy | None = None
+
+ def matches_address(self, value: str) -> bool:
+ """Whether *value* matches this asset's chain address format (False when no regex is declared)."""
+ return bool(self.address_regex) and isinstance(value, str) and re.fullmatch(self.address_regex, value) is not None
+
+
+@dataclass(frozen=True)
+class Route:
+ id: str # "protocol:source->destination"
+ protocol: str # "xreserve" | "hyperlane"
+ environment: str
+ source_asset_id: str
+ destination_asset_id: str
+ availability: str # "active" | "metadata-required" | "disabled"
+ deployment_id: str | None = None
+ source: str | None = None
+ metadata: Mapping[str, "str | int | bool"] = field(default_factory=dict)
+
+ @property
+ def active(self) -> bool:
+ return self.availability == "active"
+
+ def meta_str(self, key: str) -> str:
+ value = self.metadata.get(key)
+ if not isinstance(value, str) or not value:
+ raise ConfigurationError(f"Route metadata {key} is missing: {self.id}")
+ return value
+
+ def meta_int(self, key: str, default: int | None = None) -> int:
+ value = self.metadata.get(key, default)
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
+ raise ConfigurationError(f"Route metadata {key} is invalid: {self.id}")
+ return value
+
+
+def _parse_asset_ref(ref: Any) -> tuple[str, str]:
+ if isinstance(ref, tuple) and len(ref) == 2:
+ return str(ref[0]).lower(), str(ref[1]).lower()
+ if isinstance(ref, str) and ref.count("/") == 1:
+ chain, key = ref.split("/")
+ return chain.lower(), key.lower()
+ raise RouteNotFoundError(f"Asset reference must be 'chain/key' or (chain, key), got {ref!r}")
+
+
+class Registry:
+ """Immutable snapshot with case-insensitive lookups. Build with :func:`build_registry` or directly."""
+
+ def __init__(self, version: str, chains: Iterable[Chain], assets: Iterable[Asset], routes: Iterable[Route]) -> None:
+ self.version = version
+ self._chains = tuple(chains)
+ self._assets = tuple(assets)
+ self._routes = tuple(routes)
+ self._chain_by_id = {c.id: c for c in self._chains}
+ self._asset_by_id = {a.id: a for a in self._assets}
+ self._route_by_id = {r.id: r for r in self._routes}
+
+ def __repr__(self) -> str:
+ return f"Registry(version={self.version!r}, chains={len(self._chains)}, assets={len(self._assets)}, routes={len(self._routes)})"
+
+ # ── chains ──
+ def chains(self, environment: str | None = None) -> list[Chain]:
+ return [c for c in self._chains if environment is None or c.environment == environment]
+
+ def chain(self, chain_id: str) -> Chain:
+ for c in self._chains:
+ if c.id.lower() == str(chain_id).lower():
+ return c
+ raise RouteNotFoundError(f"Unknown bridge chain: {chain_id}")
+
+ # ── assets ──
+ def assets(self, chain: str | None = None, symbol: str | None = None, environment: str | None = None) -> list[Asset]:
+ out = []
+ for a in self._assets:
+ if chain is not None and a.chain_id.lower() != chain.lower():
+ continue
+ if symbol is not None and a.symbol.lower() != symbol.lower():
+ continue
+ if environment is not None and self._chain_by_id[a.chain_id].environment != environment:
+ continue
+ out.append(a)
+ return out
+
+ def asset(self, ref: Any) -> Asset:
+ if isinstance(ref, Asset):
+ return ref
+ chain, key = _parse_asset_ref(ref)
+ for a in self._assets:
+ if a.chain_id.lower() == chain and a.key.lower() == key:
+ return a
+ raise RouteNotFoundError(f"Unknown bridge asset: {chain}/{key}")
+
+ # ── routes ──
+ def _endpoint_matches(self, asset_id: str, selector: str | None) -> bool:
+ if selector is None:
+ return True
+ asset = self._asset_by_id[asset_id]
+ if "/" in selector:
+ return asset.id.lower() == selector.lower()
+ return asset.chain_id.lower() == selector.lower()
+
+ def routes(self, source: str | None = None, destination: str | None = None, protocol: str | None = None,
+ symbol: str | None = None, include_unavailable: bool = False, environment: str | None = None) -> list[Route]:
+ """Filter routes; *source*/*destination* accept a chain id or an ``"chain/key"`` asset ref.
+ Disabled routes are hidden unless *include_unavailable*; metadata-required routes are always listed."""
+ out = []
+ for r in self._routes:
+ if not include_unavailable and r.availability == "disabled":
+ continue
+ if environment is not None and r.environment != environment:
+ continue
+ if protocol is not None and r.protocol != protocol:
+ continue
+ if not self._endpoint_matches(r.source_asset_id, source) or not self._endpoint_matches(r.destination_asset_id, destination):
+ continue
+ if symbol is not None:
+ symbols = {self._asset_by_id[r.source_asset_id].symbol.lower(), self._asset_by_id[r.destination_asset_id].symbol.lower()}
+ if symbol.lower() not in symbols:
+ continue
+ out.append(r)
+ return out
+
+ def route(self, route_id: str) -> Route:
+ try:
+ return self._route_by_id[route_id]
+ except KeyError:
+ raise RouteNotFoundError(f"Unknown bridge route: {route_id}") from None
+
+ def find_route(self, source: Any, destination: Any, protocol: str | None = None) -> Route:
+ """prepare()'s lookup: the single non-disabled route for an exact asset pair (metadata-required included)."""
+ src, dst = self.asset(source), self.asset(destination)
+ matches = [r for r in self._routes
+ if r.source_asset_id == src.id and r.destination_asset_id == dst.id
+ and r.availability != "disabled" and (protocol is None or r.protocol == protocol)]
+ if not matches:
+ raise RouteNotFoundError(
+ f"No bridge route from {src.id} to {dst.id}" + (f" over {protocol}" if protocol else "")
+ + "; list candidates with registry.routes(source=..., destination=...)")
+ if len(matches) > 1:
+ raise AmbiguousRouteError(
+ f"{len(matches)} routes from {src.id} to {dst.id}: {[r.id for r in matches]} — pass protocol=")
+ return matches[0]
+
+
+def validate_registry(registry: Registry) -> Registry:
+ """Port of veil ``validateBridgeRegistry`` (+ the Solana metadata gate). Returns *registry* unchanged."""
+ if not registry.version.strip():
+ raise ConfigurationError("Bridge registry version must not be empty")
+ chain_ids: set[str] = set()
+ for chain in registry._chains:
+ if chain.id in chain_ids:
+ raise ConfigurationError(f"Duplicate bridge chain id: {chain.id}")
+ if chain.family not in FAMILIES:
+ raise ConfigurationError(f"Bridge chain {chain.id} has unsupported family {chain.family!r}")
+ chain_ids.add(chain.id)
+ asset_ids: set[str] = set()
+ asset_keys: set[str] = set()
+ for asset in registry._assets:
+ if asset.id in asset_ids:
+ raise ConfigurationError(f"Duplicate bridge asset id: {asset.id}")
+ if asset.chain_id not in chain_ids:
+ raise ConfigurationError(f"Bridge asset {asset.id} references unknown chain {asset.chain_id}")
+ if not asset.key.strip():
+ raise ConfigurationError(f"Bridge asset {asset.id} has an empty key")
+ scoped = f"{asset.chain_id}/{asset.key}"
+ if scoped in asset_keys:
+ raise ConfigurationError(f"Duplicate bridge asset key: {scoped}")
+ if isinstance(asset.decimals, bool) or not isinstance(asset.decimals, int) or asset.decimals < 0:
+ raise ConfigurationError(f"Bridge asset {asset.id} has invalid decimals {asset.decimals}")
+ if asset.address_regex:
+ try:
+ re.compile(asset.address_regex)
+ except re.error as exc:
+ raise ConfigurationError(f"Bridge asset {asset.id} has an invalid address validation regex") from exc
+ if asset.privacy is not None:
+ if registry._chain_by_id[asset.chain_id].family != "aleo":
+ raise ConfigurationError(f"Bridge asset {asset.id} declares a privacy capability on a non-Aleo chain")
+ if not asset.privacy.program.strip():
+ raise ConfigurationError(f"Bridge asset {asset.id} has an empty privacy program")
+ if asset.privacy.kind not in PRIVACY_KINDS:
+ raise ConfigurationError(f"Bridge asset {asset.id} has an unsupported privacy capability kind")
+ asset_ids.add(asset.id)
+ asset_keys.add(scoped)
+ route_ids: set[str] = set()
+ for route in registry._routes:
+ if route.id in route_ids:
+ raise ConfigurationError(f"Duplicate bridge route id: {route.id}")
+ if route.source_asset_id not in asset_ids:
+ raise ConfigurationError(f"Bridge route {route.id} references unknown source asset {route.source_asset_id}")
+ if route.destination_asset_id not in asset_ids:
+ raise ConfigurationError(f"Bridge route {route.id} references unknown destination asset {route.destination_asset_id}")
+ if route.availability not in AVAILABILITIES:
+ raise ConfigurationError(f"Bridge route {route.id} has unsupported availability {route.availability!r}")
+ source_chain = registry._chain_by_id[registry._asset_by_id[route.source_asset_id].chain_id]
+ destination_chain = registry._chain_by_id[registry._asset_by_id[route.destination_asset_id].chain_id]
+ if source_chain.environment != route.environment or destination_chain.environment != route.environment:
+ raise ConfigurationError(f"Bridge route {route.id} crosses registry environments")
+ if route.protocol == "hyperlane" and route.availability == "active" and source_chain.family == "solana":
+ for key in _SOLANA_REQUIRED_METADATA:
+ value = route.metadata.get(key)
+ ok = isinstance(value, int) and not isinstance(value, bool) if key == "destinationDomain" \
+ else isinstance(value, str) and bool(value)
+ if not ok:
+ raise ConfigurationError(f"Bridge route {route.id} is active but missing required Solana Hyperlane metadata")
+ route_ids.add(route.id)
+ return registry
+
+
+def build_registry(data: ModuleType = _registry_data) -> Registry:
+ """Turn the plain-dict literals of a data module into a validated :class:`Registry`."""
+ chains = [Chain(c["id"], c["displayName"], c["family"], c["environment"], c["nativeCurrencySymbol"],
+ dict(c.get("protocolDomains", {}))) for c in data.CHAINS]
+ assets = []
+ for a in data.ASSETS:
+ loc = a.get("locator")
+ priv = a.get("privacy")
+ assets.append(Asset(a["id"], a["key"], a["chainId"], a["symbol"], a["name"], a["decimals"], a["kind"],
+ Locator(loc["kind"], loc["value"], loc.get("tokenId")) if loc else None,
+ a.get("addressValidationRegex"),
+ Privacy(priv["kind"], priv["program"]) if priv else None))
+ routes = [Route(r["id"], r["protocol"], r["environment"], r["sourceAssetId"], r["destinationAssetId"],
+ r["availability"], r.get("deploymentId"), r.get("source"), dict(r.get("metadata", {})))
+ for r in data.ROUTES]
+ return validate_registry(Registry(data.REGISTRY_VERSION, chains, assets, routes))
+
+
+DEFAULT_REGISTRY: Registry = build_registry()
+
+__all__ = ["Asset", "Chain", "DEFAULT_REGISTRY", "Locator", "Privacy", "Registry", "Route", "build_registry", "validate_registry"]
diff --git a/bridge-sdk/python/aleo_bridge/sol.py b/bridge-sdk/python/aleo_bridge/sol.py
new file mode 100644
index 0000000..eb4717b
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/sol.py
@@ -0,0 +1,980 @@
+"""Solana transport, connection and the ``bridge.sol`` module (SOL → Aleo over Hyperlane).
+
+solders is imported lazily through :func:`_libs`; an install without the ``solana`` extra
+raises :class:`MissingExtraError` at the point of use, never at import. Layouts live in
+:mod:`aleo_bridge._sealevel` (pure); this module adds a synchronous JSON-RPC transport
+(solana-py ≥ 0.36 is async-only), signing, broadcast and status polling.
+"""
+from __future__ import annotations
+
+import asyncio
+import base64
+import inspect
+import json
+import os
+import threading
+import time
+from dataclasses import dataclass, replace
+from typing import Any, Callable, Mapping, Protocol, Sequence, runtime_checkable
+
+import requests
+
+from . import _sealevel as sl
+from ._calls import SolCall
+from ._plan import build_plan
+from .encoding import aleo_address_to_bytes32
+from .errors import (
+ BridgeError,
+ CheckpointInvalidError,
+ ConfigurationError,
+ InsufficientBalanceError,
+ InvalidAmountError,
+ InvalidRecipientError,
+ MissingExtraError,
+ RegistryVersionMismatchError,
+ RouteNotFoundError,
+ UnsupportedRouteError,
+)
+from .registry import Route
+from .types import DispatchReceipt, Fee, Plan, Receipt, SolanaHyperlaneQuote, Status, Step
+from .units import format_decimal_amount, resolve_amount
+
+DEFAULT_SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"
+CONFIRMED = "confirmed"
+COMMITMENTS = ("processed", "confirmed", "finalized")
+SOLANA_CHAIN_ID = "solana"
+SOLANA_SOL_ASSET_ID = "solana/sol"
+ALEO_SOL_ASSET_ID = "aleo/sol"
+
+
+@dataclass(frozen=True)
+class _SolanaLibs:
+ Keypair: Any
+ Pubkey: Any
+ Signature: Any
+ Hash: Any
+ Instruction: Any
+ AccountMeta: Any
+ MessageV0: Any
+ to_bytes_versioned: Any
+ VersionedTransaction: Any
+ set_compute_unit_limit: Any
+
+
+_LIBS: _SolanaLibs | None = None
+
+
+def _libs() -> _SolanaLibs:
+ """Import solders once; translate a missing extra into MissingExtraError."""
+ global _LIBS
+ if _LIBS is None:
+ try:
+ from solders.compute_budget import set_compute_unit_limit
+ from solders.hash import Hash
+ from solders.instruction import AccountMeta, Instruction
+ from solders.keypair import Keypair
+ from solders.message import MessageV0, to_bytes_versioned
+ from solders.pubkey import Pubkey
+ from solders.signature import Signature
+ from solders.transaction import VersionedTransaction
+ except ImportError as exc:
+ raise MissingExtraError("solana", "Solana connections and SOL transfers") from exc
+ _LIBS = _SolanaLibs(Keypair, Pubkey, Signature, Hash, Instruction, AccountMeta, MessageV0,
+ to_bytes_versioned, VersionedTransaction, set_compute_unit_limit)
+ return _LIBS
+
+
+# --- synchronous JSON-RPC transport (veil src/solana/rpc.ts) ------------------------------------
+
+@dataclass(frozen=True)
+class SendOptions:
+ """Broadcast options; attribute-compatible with solana-py's ``TxOpts``."""
+ skip_preflight: bool = False
+ preflight_commitment: str = CONFIRMED
+ skip_confirmation: bool = True
+
+
+@dataclass(frozen=True)
+class RpcResult:
+ value: Any
+
+
+@dataclass(frozen=True)
+class LatestBlockhash:
+ blockhash: Any # solders Hash
+ last_valid_block_height: int
+
+
+@dataclass(frozen=True)
+class AccountInfo:
+ data: bytes
+ lamports: int
+ owner: str
+
+
+@dataclass(frozen=True)
+class SignatureStatus:
+ err: Any
+ confirmation_status: str | None
+
+
+@dataclass(frozen=True)
+class TransactionMeta:
+ log_messages: list[str] | None
+
+
+@dataclass(frozen=True)
+class TransactionWithMeta:
+ meta: TransactionMeta | None
+
+
+@dataclass(frozen=True)
+class ConfirmedTransaction:
+ transaction: TransactionWithMeta
+ slot: int
+
+
+class SolanaRpcClient:
+ """Minimal synchronous Solana JSON-RPC client exposing the solana-py method surface SolModule uses.
+
+ Every method issues one POST, validates the envelope (HTTP status, JSON, JSON-RPC ``error``,
+ ``result`` presence) and the result shape, and returns an object with ``.value`` shaped like
+ solana-py's response types. No method signs or retries.
+ """
+
+ def __init__(self, url: str, *, commitment: str = CONFIRMED, session: Any = None, timeout: float = 30.0) -> None:
+ if commitment not in COMMITMENTS:
+ raise ConfigurationError(f"Solana commitment must be one of {COMMITMENTS}, got {commitment!r}")
+ self.url = url
+ self.commitment = commitment
+ self.timeout = timeout
+ self._session = session or requests.Session()
+
+ def _commitment(self, commitment: str | None) -> dict[str, str]:
+ return {"commitment": str(commitment or self.commitment)}
+
+ def _call(self, method: str, params: list[Any]) -> Any:
+ try:
+ response = self._session.post(self.url, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
+ timeout=self.timeout, headers={"content-type": "application/json", "cache-control": "no-cache"})
+ except requests.RequestException as exc:
+ raise BridgeError(f"Solana RPC {method} request failed: {exc}") from exc
+ if not 200 <= response.status_code < 300:
+ raise BridgeError(f"Solana RPC {method} request failed with HTTP status {response.status_code}")
+ try:
+ body = response.json()
+ except ValueError as exc:
+ raise BridgeError(f"Solana RPC {method} returned invalid JSON") from exc
+ if not isinstance(body, dict):
+ raise BridgeError(f"Solana RPC {method} returned an invalid JSON-RPC response")
+ error = body.get("error")
+ if error:
+ details = f"; {json.dumps(error['data'])}" if isinstance(error, dict) and "data" in error else ""
+ message = error.get("message", "unknown error") if isinstance(error, dict) else str(error)
+ raise BridgeError(f"Solana RPC {method} returned a JSON-RPC error: {message}{details}")
+ if "result" not in body:
+ raise BridgeError(f"Solana RPC {method} returned an invalid result envelope")
+ return body["result"]
+
+ @staticmethod
+ def _integer(method: str, value: Any) -> int:
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
+ raise BridgeError(f"Solana RPC {method} returned an invalid result")
+ return value
+
+ @staticmethod
+ def _contextual(method: str, result: Any) -> Any:
+ if not isinstance(result, dict) or "value" not in result:
+ raise BridgeError(f"Solana RPC {method} returned an invalid contextual result")
+ return result["value"]
+
+ def get_latest_blockhash(self, commitment: str | None = None) -> RpcResult:
+ value = self._contextual("getLatestBlockhash", self._call("getLatestBlockhash", [self._commitment(commitment)]))
+ if not isinstance(value, dict) or not isinstance(value.get("blockhash"), str) or not value["blockhash"]:
+ raise BridgeError("Solana RPC getLatestBlockhash returned an invalid result")
+ return RpcResult(LatestBlockhash(_libs().Hash.from_string(value["blockhash"]),
+ self._integer("getLatestBlockhash", value.get("lastValidBlockHeight"))))
+
+ def get_block_height(self, commitment: str | None = None) -> RpcResult:
+ return RpcResult(self._integer("getBlockHeight", self._call("getBlockHeight", [self._commitment(commitment)])))
+
+ def is_blockhash_valid(self, blockhash: Any, commitment: str | None = None) -> RpcResult:
+ value = self._contextual("isBlockhashValid", self._call("isBlockhashValid", [str(blockhash), self._commitment(commitment)]))
+ if not isinstance(value, bool):
+ raise BridgeError("Solana RPC isBlockhashValid returned an invalid result")
+ return RpcResult(value)
+
+ def get_balance(self, pubkey: Any, commitment: str | None = None) -> RpcResult:
+ value = self._contextual("getBalance", self._call("getBalance", [str(pubkey), self._commitment(commitment)]))
+ return RpcResult(self._integer("getBalance", value))
+
+ def get_account_info(self, pubkey: Any, commitment: str | None = None, encoding: str = "base64") -> RpcResult:
+ if encoding != "base64":
+ raise BridgeError("SolanaRpcClient.get_account_info supports base64 encoding only")
+ value = self._contextual("getAccountInfo", self._call("getAccountInfo", [str(pubkey), {"encoding": "base64", **self._commitment(commitment)}]))
+ if value is None:
+ return RpcResult(None)
+ data = value.get("data") if isinstance(value, dict) else None
+ if not isinstance(data, list) or len(data) != 2 or not isinstance(data[0], str) or data[1] != "base64":
+ raise BridgeError("Solana RPC getAccountInfo returned invalid base64 account data")
+ try:
+ raw = base64.b64decode(data[0], validate=True)
+ except ValueError as exc:
+ raise BridgeError("Solana RPC getAccountInfo returned invalid base64 account data") from exc
+ return RpcResult(AccountInfo(raw, int(value.get("lamports", 0)), str(value.get("owner", ""))))
+
+ def get_fee_for_message(self, message: Any, commitment: str | None = None) -> RpcResult:
+ raw = bytes(message) if isinstance(message, (bytes, bytearray)) else _libs().to_bytes_versioned(message)
+ value = self._contextual("getFeeForMessage", self._call("getFeeForMessage", [base64.b64encode(raw).decode(), self._commitment(commitment)]))
+ return RpcResult(None if value is None else self._integer("getFeeForMessage", value))
+
+ def get_minimum_balance_for_rent_exemption(self, usize: int, commitment: str | None = None) -> RpcResult:
+ if isinstance(usize, bool) or not isinstance(usize, int) or usize < 0:
+ raise BridgeError("Solana rent data length must be a non-negative integer")
+ return RpcResult(self._integer("getMinimumBalanceForRentExemption",
+ self._call("getMinimumBalanceForRentExemption", [usize, self._commitment(commitment)])))
+
+ def send_raw_transaction(self, txn: bytes, opts: Any = None) -> RpcResult:
+ opts = opts or SendOptions()
+ config = {"encoding": "base64", "skipPreflight": bool(opts.skip_preflight), "preflightCommitment": str(opts.preflight_commitment)}
+ result = self._call("sendTransaction", [base64.b64encode(bytes(txn)).decode(), config])
+ if not isinstance(result, str) or not result:
+ raise BridgeError("Solana RPC sendTransaction returned an invalid signature")
+ return RpcResult(_libs().Signature.from_string(result))
+
+ def close(self) -> None:
+ """Release the HTTP session (and its connection pool). Idempotent; ``requests`` allows reuse
+ afterwards, so a closed client that is used again simply opens fresh connections."""
+ closer = getattr(self._session, "close", None)
+ if callable(closer):
+ closer()
+
+ def get_signature_statuses(self, signatures: Sequence[Any], search_transaction_history: bool = False) -> RpcResult:
+ value = self._contextual("getSignatureStatuses", self._call(
+ "getSignatureStatuses", [[str(s) for s in signatures], {"searchTransactionHistory": bool(search_transaction_history)}]))
+ if not isinstance(value, list) or len(value) != len(signatures):
+ raise BridgeError("Solana RPC getSignatureStatuses returned an invalid result")
+ statuses: list[SignatureStatus | None] = []
+ for status in value:
+ if status is None:
+ statuses.append(None)
+ continue
+ if not isinstance(status, dict) or "err" not in status:
+ raise BridgeError("Solana RPC getSignatureStatuses returned an invalid status")
+ confirmation = status.get("confirmationStatus")
+ if confirmation is not None and confirmation not in COMMITMENTS:
+ raise BridgeError(f"Solana RPC getSignatureStatuses returned unsupported confirmation status: {confirmation}")
+ statuses.append(SignatureStatus(status["err"], confirmation))
+ return RpcResult(statuses)
+
+ def get_transaction(self, tx_sig: Any, encoding: str = "json", commitment: str | None = None,
+ max_supported_transaction_version: int | None = None) -> RpcResult:
+ config: dict[str, Any] = {"encoding": encoding, **self._commitment(commitment)}
+ if max_supported_transaction_version is not None:
+ config["maxSupportedTransactionVersion"] = max_supported_transaction_version
+ result = self._call("getTransaction", [str(tx_sig), config])
+ if result is None:
+ return RpcResult(None)
+ if not isinstance(result, dict) or "meta" not in result:
+ raise BridgeError("Solana RPC getTransaction returned an invalid result")
+ meta = result["meta"]
+ if meta is None:
+ return RpcResult(ConfirmedTransaction(TransactionWithMeta(None), int(result.get("slot", 0))))
+ if not isinstance(meta, dict) or "logMessages" not in meta:
+ raise BridgeError("Solana RPC getTransaction returned invalid metadata")
+ logs = meta["logMessages"]
+ if logs is not None and (not isinstance(logs, list) or not all(isinstance(line, str) for line in logs)):
+ raise BridgeError("Solana RPC getTransaction returned invalid logs")
+ return RpcResult(ConfirmedTransaction(TransactionWithMeta(TransactionMeta(logs)), int(result.get("slot", 0))))
+
+
+class _AsyncClientAdapter:
+ """Drives a solana-py ``AsyncClient`` (0.36+ is async-only) synchronously on a private event-loop
+ thread, and supplies ``is_blockhash_valid`` (missing from solana-py) and ``send_raw_transaction``
+ with our ``SendOptions`` translated to ``TxOpts``. Other methods are forwarded unchanged."""
+
+ def __init__(self, client: Any) -> None:
+ self._client = client
+ self._loop = asyncio.new_event_loop()
+ self._thread = threading.Thread(target=self._loop.run_forever, name="aleo-bridge-solana-rpc", daemon=True)
+ self._thread.start()
+ self._closed = False
+
+ def _run(self, coroutine: Any, timeout: float | None = None) -> Any:
+ return asyncio.run_coroutine_threadsafe(coroutine, self._loop).result(timeout)
+
+ def close(self, timeout: float = 5.0) -> None:
+ """Close the wrapped client, then stop the private loop thread. Idempotent.
+
+ The wrapped client's own ``close()`` runs FIRST and on our loop: solana-py's ``AsyncClient``
+ owns an aiohttp session that can only be closed from the loop it was created on, so stopping
+ the thread first would leak the connection pool. It is bounded by *timeout* and its failure is
+ swallowed — a third-party transport that cannot close must not leave our loop thread running
+ forever — and the loop is stopped either way. ``_closed`` is set only once that stop has been
+ issued, so a raising ``close()`` cannot turn the next call into a no-op over a live thread.
+ The loop itself is closed only after the thread has actually exited; closing a running loop raises.
+ """
+ if self._closed:
+ return
+ try:
+ closer = getattr(self._client, "close", None)
+ if callable(closer):
+ result = closer()
+ if inspect.isawaitable(result):
+ self._run(result, timeout=timeout)
+ except Exception: # noqa: BLE001 — including a _run timeout
+ pass # best-effort release; the thread still has to stop
+ finally:
+ self._loop.call_soon_threadsafe(self._loop.stop)
+ self._closed = True
+ self._thread.join(timeout=timeout)
+ if self._thread.is_alive():
+ return # still running: leave the loop alone rather than raise
+ if not self._loop.is_closed():
+ self._loop.close()
+
+ def __getattr__(self, name: str) -> Any:
+ attribute = getattr(self._client, name)
+ if inspect.iscoroutinefunction(attribute):
+ return lambda *args, **kwargs: self._run(attribute(*args, **kwargs))
+ return attribute
+
+ def is_blockhash_valid(self, blockhash: Any, commitment: str | None = None) -> Any:
+ from solders.commitment_config import CommitmentLevel
+ from solders.rpc.config import RpcContextConfig
+ from solders.rpc.requests import IsBlockhashValid
+ from solders.rpc.responses import IsBlockhashValidResp
+
+ level = {"processed": CommitmentLevel.Processed, "confirmed": CommitmentLevel.Confirmed,
+ "finalized": CommitmentLevel.Finalized}[commitment or CONFIRMED]
+ request = IsBlockhashValid(blockhash, RpcContextConfig(commitment=level))
+ return self._run(self._client._provider.make_request(request, IsBlockhashValidResp))
+
+ def send_raw_transaction(self, txn: bytes, opts: Any = None) -> Any:
+ try:
+ from solana.rpc.models import TxOpts
+ except ImportError: # solana-py < 0.36 kept TxOpts in solana.rpc.types
+ try:
+ from solana.rpc.types import TxOpts
+ except ImportError as exc:
+ raise MissingExtraError("solana", "solana-py AsyncClient transport") from exc
+ opts = opts or SendOptions()
+ tx_opts = TxOpts(skip_confirmation=True, skip_preflight=bool(opts.skip_preflight), preflight_commitment=str(opts.preflight_commitment))
+ return self._run(self._client.send_raw_transaction(bytes(txn), tx_opts))
+
+
+@runtime_checkable
+class SolanaSigner(Protocol):
+ """solana-py's signer shape: a solders ``Keypair`` or any wallet exposing these two methods."""
+
+ def pubkey(self) -> Any: ...
+
+ def sign_message(self, message: bytes) -> Any: ...
+
+
+def keypair_from_private_key(private_key: str | bytes) -> Any:
+ """Parse a Solana secret: base58 (Phantom export), a JSON array of 64 ints (solana-cli ``id.json``),
+ 64 raw bytes (seed ‖ pubkey) or a 32-byte seed."""
+ libs = _libs()
+ if isinstance(private_key, (bytes, bytearray, memoryview)):
+ raw = bytes(private_key)
+ else:
+ # Every failure below raises OUTSIDE its except block, so neither __cause__ nor __context__ is
+ # set: a chained JSONDecodeError carries the whole document in .doc, and a solders parse error
+ # may echo its input — either one would print the private key with the traceback.
+ text = private_key.strip()
+ if text.startswith("["):
+ values: Any = None
+ malformed = False
+ try:
+ values = json.loads(text)
+ except ValueError:
+ malformed = True
+ if malformed:
+ raise ConfigurationError(
+ "Solana private key JSON array is malformed; expected the 64 integers of a solana-cli id.json")
+ if not isinstance(values, list) or not all(isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 255 for v in values):
+ raise ConfigurationError("Solana private key JSON array must hold integers 0–255")
+ raw = bytes(values)
+ else:
+ keypair = None
+ try:
+ keypair = libs.Keypair.from_base58_string(text)
+ except Exception: # noqa: BLE001 — solders raises its own parse error types
+ keypair = None
+ if keypair is None:
+ raise ConfigurationError("Solana private key is not a valid base58 64-byte secret")
+ return keypair
+ if len(raw) == 64:
+ return libs.Keypair.from_bytes(raw)
+ if len(raw) == 32:
+ return libs.Keypair.from_seed(raw)
+ raise ConfigurationError(f"Solana private key must be 64 bytes (seed || pubkey) or a 32-byte seed, got {len(raw)}")
+
+
+class Solana:
+ """Solana transport plus an optional signer (spec §3.2).
+
+ ``Solana(rpc_url)`` builds ``SolanaRpcClient(rpc_url, commitment="confirmed")``;
+ ``Solana(client=…)`` reuses a caller-configured client: anything with the solana-py read/send
+ method surface (its own commitment, timeout, headers), or a solana-py ``AsyncClient``, which is
+ driven synchronously through :class:`_AsyncClientAdapter`. Exactly one of ``rpc_url``/``client``
+ may be given; at most one of ``signer``/``private_key``. A connection without a signer is
+ read-only (quotes and status reads work, ``send`` does not).
+ """
+
+ def __init__(self, rpc_url: str | None = None, *, client: Any = None, signer: Any = None,
+ private_key: str | bytes | None = None) -> None:
+ if rpc_url is not None and client is not None:
+ raise ConfigurationError("Solana(): pass rpc_url or client, not both")
+ if signer is not None and private_key is not None:
+ raise ConfigurationError("Solana(): pass signer or private_key, not both")
+ if client is None:
+ _libs() # SolanaRpcClient returns solders Hash/Signature values
+ rpc_url = rpc_url or DEFAULT_SOLANA_RPC_URL
+ client = SolanaRpcClient(rpc_url, commitment=CONFIRMED)
+ elif inspect.iscoroutinefunction(getattr(client, "get_balance", None)):
+ client = _AsyncClientAdapter(client) # solana-py ≥ 0.36 AsyncClient
+ self._rpc_url = rpc_url
+ self._client = client
+ if private_key is not None:
+ signer = keypair_from_private_key(private_key)
+ if signer is not None and not (callable(getattr(signer, "pubkey", None)) and callable(getattr(signer, "sign_message", None))):
+ raise ConfigurationError("Solana signer must expose pubkey() and sign_message(bytes) — a solders Keypair or a solana-py Signer")
+ self._signer = signer
+
+ @classmethod
+ def from_env(cls, env: Mapping[str, str] | None = None) -> "Solana | None":
+ """Private key from ``SOLANA_PRIVATE_KEY`` else ``BRIDGE_SOLANA_PRIVATE_KEY`` (the user's shell
+ exports the latter); RPC from ``SOLANA_RPC_URL`` else ``BRIDGE_LIVE_SOLANA_RPC_URL`` else the
+ default. A key (either name) → signing connection; URL alone → read-only; neither → ``None``."""
+ env = os.environ if env is None else env
+ key = env.get("SOLANA_PRIVATE_KEY") or env.get("BRIDGE_SOLANA_PRIVATE_KEY")
+ url = env.get("SOLANA_RPC_URL") or env.get("BRIDGE_LIVE_SOLANA_RPC_URL") or None
+ if key:
+ return cls(url, private_key=key)
+ if url:
+ return cls(url)
+ return None
+
+ @property
+ def client(self) -> Any:
+ return self._client
+
+ @property
+ def signer(self) -> Any:
+ return self._signer
+
+ @property
+ def rpc_url(self) -> str | None:
+ return self._rpc_url
+
+ @property
+ def can_sign(self) -> bool:
+ return self._signer is not None
+
+ @property
+ def pubkey(self) -> Any:
+ if self._signer is None:
+ raise ConfigurationError("Solana connection is read-only: pass signer= or private_key= to Solana() to sign")
+ return self._signer.pubkey()
+
+ @property
+ def address(self) -> str | None:
+ return None if self._signer is None else str(self._signer.pubkey())
+
+ def sign_message(self, message: bytes) -> Any:
+ """Fee-payer signature over compiled message bytes (``to_bytes_versioned`` for v0 messages)."""
+ if self._signer is None:
+ raise ConfigurationError("Solana connection is read-only: pass signer= or private_key= to Solana() to sign")
+ return self._signer.sign_message(bytes(message))
+
+ def close(self) -> None:
+ """Release the wrapped client's resources (idempotent).
+
+ The default transport closes its HTTP session; an adapted async solana-py client closes the
+ client itself and then its private event-loop thread. A client with no ``close()`` is a no-op.
+ ``__exit__`` swallows whatever this raises; call it directly to see the error."""
+ closer = getattr(self._client, "close", None)
+ if callable(closer):
+ closer()
+
+ def __enter__(self) -> "Solana":
+ return self
+
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
+ try:
+ self.close()
+ except Exception: # noqa: BLE001 — releasing a transport must never
+ pass # replace (or invent) the caller's own exception
+
+
+def _confirmation_name(status: Any) -> str | None:
+ """Normalise solders' TransactionConfirmationStatus enum (or a plain string) to 'processed'|'confirmed'|'finalized'."""
+ value = getattr(status, "confirmation_status", None)
+ if value is None:
+ return None
+ name = getattr(value, "name", None) or str(value)
+ return str(name).rsplit(".", 1)[-1].lower()
+
+
+def _assert_amount_matches_plan(plan: Plan, *, amount: str | None, amount_atomic: int | None, decimals: int) -> None:
+ """A plan pins the amount. Re-stating the same one is harmless; a different one is a mistake the
+ caller must see, not a silent override of the plan they prepared (``EthModule``'s rule)."""
+ if amount is None and amount_atomic is None:
+ return
+ given = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=decimals)
+ if given != plan.amount_atomic:
+ raise ValueError(f"plan is for {plan.amount_atomic} atomic units, but {given} was passed; "
+ "pass plan= alone or re-run quote() for the amount you want")
+
+
+@dataclass
+class SolBuild:
+ """Everything ``send`` needs after ``build``: the quote, the compiled message, the partially signed
+ transaction, the unique-message address that seeds the PDAs, and the blockhash lifetime."""
+ quote: SolanaHyperlaneQuote
+ message: Any
+ transaction: Any
+ unique_message_address: str
+ blockhash: str
+ last_valid_block_height: int
+ sender: str
+ destination_domain: int
+
+
+def _signature_status(client: Any, signature: Any) -> str | None:
+ """'failed' | 'processed' | 'confirmed' | 'finalized' | None (unknown); raises on RPC errors.
+
+ ``None`` means one thing only: the node has NO row for this signature. A row that exists but
+ carries no ``confirmationStatus`` (and no ``err``) is a transaction the node has seen — processed
+ at least — so it reports ``'processed'``. Collapsing that into ``None`` would send both callers to
+ the blockhash probe and let a landed transfer be reported EXPIRED, inviting a resend.
+ """
+ value = client.get_signature_statuses([signature], search_transaction_history=True).value
+ status = value[0] if value else None
+ if status is None:
+ return None
+ if getattr(status, "err", None) is not None:
+ return "failed"
+ name = _confirmation_name(status)
+ if name is None:
+ return "processed"
+ if name not in ("processed", "confirmed", "finalized"):
+ raise BridgeError(f"Solana RPC getSignatureStatuses returned unsupported confirmation status: {name}")
+ return name
+
+
+def _poll_for_confirmation(client: Any, signature: str, blockhash: str, timeout_seconds: float,
+ poll_seconds: float) -> str | None:
+ """Poll until confirmed/finalized ('confirmed'|'finalized'), the blockhash expires ('expired'), or the
+ deadline passes (None). A status read that raises is swallowed — the transaction is already broadcast,
+ so a transient RPC error must not be reported as a transfer failure. An on-chain ``err`` raises.
+
+ The blockhash probe runs only while the signature has NO status at all: a ``processed`` transaction has
+ already landed, and reporting it 'expired' would invite a resend (the same rule ``source_status`` applies).
+ ``processed`` therefore keeps polling until it confirms or the deadline passes."""
+ libs = _libs()
+ sig = libs.Signature.from_string(signature)
+ hash_ = libs.Hash.from_string(blockhash)
+ interval = max(float(poll_seconds), 0.1)
+ deadline = time.monotonic() + max(float(timeout_seconds), 0.0)
+ while True:
+ try:
+ status = _signature_status(client, sig)
+ except Exception: # noqa: BLE001 — transport/decoding errors are transient here
+ status = None
+ if status == "failed":
+ raise BridgeError(f"Solana Hyperlane transfer failed on-chain: {signature}")
+ if status in ("confirmed", "finalized"):
+ return status
+ if status is None: # no status at all: only then can the blockhash have expired
+ try:
+ if not client.is_blockhash_valid(hash_, commitment=CONFIRMED).value:
+ return "expired"
+ except Exception: # noqa: BLE001
+ pass # advisory while the signature may still land
+ if time.monotonic() >= deadline:
+ return None
+ time.sleep(interval)
+
+
+class SolModule:
+ """``bridge.sol`` — Solana-origin SOL → Aleo over the Hyperlane warp route (spec §6).
+
+ Reads (``balance``, ``quote_transfer_remote``, ``source_status``) work on a read-only
+ connection; ``transfer_remote(...).send()`` needs a signer. Route metadata is re-validated
+ from the live registry on every call.
+ """
+
+ def __init__(self, bridge: Any, conn: Solana) -> None:
+ self._bridge = bridge
+ self.conn = conn
+
+ @property
+ def client(self) -> Any:
+ return self.conn.client
+
+ @property
+ def registry(self) -> Any:
+ return self._bridge.registry
+
+ @property
+ def environment(self) -> str:
+ return self._bridge.environment
+
+ def outbound_route(self) -> Route:
+ """The environment's SOL → Aleo Hyperlane route (RouteNotFoundError on testnet, which has none).
+
+ ``SOLANA_SOL_ASSET_ID``/``ALEO_SOL_ASSET_ID`` are fixed mainnet asset ids (there is no
+ testnet Solana chain in the registry), so ``find_route`` alone would resolve the mainnet
+ route regardless of ``self.environment``; this guards that the route's own environment
+ matches the module's, mirroring how ``EthModule`` scopes its route lookups by environment.
+ """
+ route = self.registry.find_route(SOLANA_SOL_ASSET_ID, ALEO_SOL_ASSET_ID, protocol="hyperlane")
+ if route.environment != self.environment:
+ raise RouteNotFoundError(f"No Solana Hyperlane route to Aleo for environment {self.environment!r}")
+ return route
+
+ def metadata(self, route: Route | None = None) -> sl.SolanaRouteMetadata:
+ return sl.solana_route_metadata(route or self.outbound_route())
+
+ # --- reads ------------------------------------------------------------------------------
+
+ def _pubkey(self, address: str) -> Any:
+ return _libs().Pubkey.from_string(address)
+
+ def _account_data(self, address: str) -> bytes | None:
+ value = self.client.get_account_info(self._pubkey(address), commitment=CONFIRMED, encoding="base64").value
+ if value is None:
+ return None
+ data = value.data
+ if isinstance(data, (list, tuple)): # raw JSON shape: ["", "base64"]
+ return base64.b64decode(data[0])
+ return bytes(data)
+
+ def _balance_of(self, address: str) -> int:
+ return int(self.client.get_balance(self._pubkey(address), commitment=CONFIRMED).value)
+
+ def balance(self) -> int:
+ """Lamports held by the connected wallet."""
+ address = self.conn.address
+ if address is None:
+ raise ConfigurationError("Solana connection is read-only: pass signer= or private_key= to Solana() to read the wallet balance")
+ return self._balance_of(address)
+
+ # --- quote ------------------------------------------------------------------------------
+
+ def _make_plan(self, route: Route, *, recipient: str, amount_atomic: int, sender: str) -> Plan:
+ return build_plan(self.registry, route, amount_atomic=amount_atomic, recipient=recipient, sender=sender)
+
+ def _compile_message(self, metadata: sl.SolanaRouteMetadata, *, sender: str, unique_message: str,
+ recipient32: bytes, amount_atomic: int) -> tuple[Any, str, int]:
+ """v0 message: [SetComputeUnitLimit(400_000), TransferRemote] with a confirmed blockhash."""
+ libs = _libs()
+ data = sl.build_transfer_remote_instruction_data(metadata.destination_domain, recipient32, amount_atomic)
+ metas = [libs.AccountMeta(libs.Pubkey.from_string(m.address), is_signer=m.signer, is_writable=m.writable)
+ for m in sl.account_metas(metadata, sender, unique_message)]
+ instruction = libs.Instruction(libs.Pubkey.from_string(metadata.warp_program_address), data, metas)
+ latest = self.client.get_latest_blockhash(commitment=CONFIRMED).value
+ message = libs.MessageV0.try_compile(
+ libs.Pubkey.from_string(sender),
+ [libs.set_compute_unit_limit(sl.COMPUTE_UNIT_LIMIT), instruction],
+ [],
+ latest.blockhash,
+ )
+ return message, str(latest.blockhash), int(latest.last_valid_block_height)
+
+ def quote_transfer_remote(self, recipient: str | None = None, *, amount: str | None = None,
+ amount_atomic: int | None = None, sender: str | None = None,
+ plan: Plan | None = None) -> SolanaHyperlaneQuote:
+ """Lamports required for a SOL → Aleo transfer: amount + IGP payment + network fee + rent (spec §5 kind
+ ``solana-hyperlane``). Reads Solana; never signs. ``sender`` defaults to the connected wallet and is required
+ for the fee estimate.
+
+ ``plan`` (from ``Bridge.quote``) supplies recipient, amount and sender, and must match the live registry
+ version and route; like ``EthModule`` it is mutually exclusive with ``sender=``, and an ``amount``/
+ ``amount_atomic`` that disagrees with the plan is a ``ValueError`` (an identical one is tolerated, so
+ re-stating the plan's own amount is harmless). Without a plan, ``recipient`` is required.
+ """
+ libs = _libs()
+ route = self.outbound_route()
+ metadata = sl.solana_route_metadata(route)
+ decimals = self.registry.asset(route.source_asset_id).decimals
+ if plan is not None:
+ if sender is not None:
+ raise ValueError("Pass plan= or sender=, not both: the plan carries its own sender")
+ if plan.registry_version != self.registry.version:
+ raise RegistryVersionMismatchError(
+ f"plan was prepared against registry {plan.registry_version}; this client runs {self.registry.version} — re-run quote()")
+ if plan.route_id != route.id:
+ raise UnsupportedRouteError(f"plan route {plan.route_id} is not the Solana Hyperlane route {route.id}")
+ _assert_amount_matches_plan(plan, amount=amount, amount_atomic=amount_atomic, decimals=decimals)
+ recipient, amount_atomic, amount, sender = plan.recipient, plan.amount_atomic, None, plan.sender
+ elif recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ amount_atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=decimals)
+ if amount_atomic <= 0:
+ raise InvalidAmountError("amount must be positive")
+ recipient32 = aleo_address_to_bytes32(recipient)
+ sender = sender or self.conn.address
+ if sender is None:
+ raise ConfigurationError("Solana sender is required to quote the transaction fee: configure a signer or pass sender=")
+ if plan is None:
+ plan = self._make_plan(route, recipient=recipient, amount_atomic=amount_atomic, sender=sender)
+
+ igp_data = self._account_data(metadata.igp_account)
+ if igp_data is None:
+ raise BridgeError(f"Solana IGP account does not exist: {metadata.igp_account}")
+ igp = sl.quote_igp_lamports(igp_data, metadata.destination_domain, metadata.destination_gas_amount)
+
+ unique = libs.Keypair() # disposable: only its pubkey seeds the fee-estimate message
+ message, _blockhash, _height = self._compile_message(
+ metadata, sender=sender, unique_message=str(unique.pubkey()), recipient32=recipient32, amount_atomic=amount_atomic)
+ fee_value = self.client.get_fee_for_message(message, commitment=CONFIRMED).value
+ if fee_value is None:
+ raise BridgeError("Solana RPC getFeeForMessage returned no fee (the blockhash is unknown to the node); retry")
+ fee = int(fee_value)
+ rent = sum(int(self.client.get_minimum_balance_for_rent_exemption(size).value)
+ for size in (sl.GAS_PAYMENT_ACCOUNT_DATA_LENGTH, sl.DISPATCHED_MESSAGE_ACCOUNT_DATA_LENGTH, 0))
+ total = amount_atomic + igp + fee + rent
+ fees = (
+ Fee("interchain-gas", SOLANA_CHAIN_ID, route.source_asset_id, format_decimal_amount(igp, decimals), True),
+ Fee("network", SOLANA_CHAIN_ID, route.source_asset_id, format_decimal_amount(fee, decimals), True),
+ Fee("rent", SOLANA_CHAIN_ID, route.source_asset_id, format_decimal_amount(rent, decimals), True),
+ )
+ return SolanaHyperlaneQuote(
+ kind="solana-hyperlane", plan=plan, fees=fees, amount_out=plan.amount,
+ igp_lamports=igp, network_fee_lamports=fee, rent_lamports=rent, total_lamports=total,
+ unique_message_address=str(unique.pubkey()),
+ )
+
+ # --- write ------------------------------------------------------------------------------
+
+ def transfer_remote(self, recipient: str | None = None, *, amount: str | None = None,
+ amount_atomic: int | None = None, plan: Plan | None = None) -> SolCall[DispatchReceipt]:
+ """Send native SOL to an Aleo address over the Hyperlane warp route (spec §6).
+
+ Returns a :class:`SolCall`: ``build()`` previews the partially signed transaction,
+ ``send()`` moves funds (amount + IGP payment + network fee + rent leave the wallet).
+
+ ``plan`` (from ``Bridge.execute``) supplies recipient and amount and must have been prepared for
+ the connected wallet; its registry version and route id are re-checked against the live registry
+ when the call runs. An ``amount``/``amount_atomic`` that disagrees with the plan is a
+ ``ValueError``. Without a plan, ``recipient`` is required.
+ """
+ route = self.outbound_route()
+ sl.solana_route_metadata(route) # refuse inactive/malformed routes early
+ decimals = self.registry.asset(route.source_asset_id).decimals
+ if plan is not None:
+ _assert_amount_matches_plan(plan, amount=amount, amount_atomic=amount_atomic, decimals=decimals)
+ recipient, amount_atomic, amount = plan.recipient, plan.amount_atomic, None
+ elif recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ amount_atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=decimals)
+ if amount_atomic <= 0:
+ raise InvalidAmountError("amount must be positive")
+ aleo_address_to_bytes32(recipient)
+
+ def build_result(receipt: Receipt) -> DispatchReceipt:
+ # The receipt's routeId comes from the route resolved at build time, which is the one the
+ # transaction was actually compiled against; route.id here is only the snapshot's fallback.
+ return DispatchReceipt(transaction_id=receipt.source_tx_id or receipt.id,
+ route_id=receipt.protocol_state.get("routeId") or route.id,
+ message_id=receipt.protocol_state.get("messageId"),
+ amount_atomic=amount_atomic, receipt=receipt)
+
+ return SolCall(self, route=route, recipient=recipient, amount_atomic=amount_atomic, plan=plan,
+ build_result=build_result, store=getattr(self._bridge, "checkpoints", None))
+
+ def _build_transaction(self, *, recipient: str, amount_atomic: int, plan: Plan | None) -> SolBuild:
+ libs = _libs()
+ sender = self.conn.address
+ if sender is None:
+ raise ConfigurationError("Solana connection is read-only: pass signer= or private_key= to Solana() to build transactions")
+ if plan is not None and plan.sender and plan.sender != sender:
+ raise ConfigurationError(f"Prepared sender {plan.sender} does not match connected account {sender}")
+ quote = (self.quote_transfer_remote(plan=plan) if plan is not None # the plan carries the sender
+ else self.quote_transfer_remote(recipient, amount_atomic=amount_atomic, sender=sender))
+ # Re-resolved here rather than reusing the route snapshotted when transfer_remote() was called:
+ # the registry may have moved since, and compiling the instruction against a stale deployment
+ # while the quote priced the live one would sign a transfer to the wrong program.
+ route = self.outbound_route()
+ if route.id != quote.plan.route_id:
+ raise UnsupportedRouteError(
+ f"the Solana Hyperlane route is now {route.id}, but this transfer was quoted for "
+ f"{quote.plan.route_id} — re-run quote()")
+ metadata = sl.solana_route_metadata(route)
+ unique = libs.Keypair() # fresh per build: seeds the dispatched-message and gas-payment PDAs
+ message, blockhash, last_valid_block_height = self._compile_message(
+ metadata, sender=sender, unique_message=str(unique.pubkey()),
+ recipient32=aleo_address_to_bytes32(quote.plan.recipient), amount_atomic=quote.plan.amount_atomic)
+ keys = list(message.account_keys)
+ if keys[0] != libs.Pubkey.from_string(sender):
+ raise BridgeError("compiled Solana message does not list the sender as fee payer")
+ signatures = [libs.Signature.default()] * message.header.num_required_signatures
+ signatures[keys.index(unique.pubkey())] = unique.sign_message(libs.to_bytes_versioned(message))
+ transaction = libs.VersionedTransaction.populate(message, signatures)
+ return SolBuild(quote=replace(quote, unique_message_address=str(unique.pubkey())), message=message,
+ transaction=transaction, unique_message_address=str(unique.pubkey()), blockhash=blockhash,
+ last_valid_block_height=last_valid_block_height, sender=sender,
+ destination_domain=metadata.destination_domain)
+
+ def _source_receipt(self, built: SolBuild, signature: str) -> Receipt:
+ return Receipt(
+ id=signature, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=signature,
+ protocol_state={
+ "routeId": built.quote.plan.route_id,
+ "signature": signature,
+ "uniqueMessageAddress": built.unique_message_address,
+ "destinationDomain": built.destination_domain,
+ "quotedLamports": str(built.quote.total_lamports),
+ "blockhash": built.blockhash,
+ "lastValidBlockHeight": str(built.last_valid_block_height),
+ },
+ )
+
+ def _transaction_logs(self, signature: str) -> list[str] | None:
+ libs = _libs()
+ value = self.client.get_transaction(libs.Signature.from_string(signature), encoding="json",
+ commitment=CONFIRMED, max_supported_transaction_version=0).value
+ if value is None:
+ return None
+ meta = value.transaction.meta
+ return None if meta is None else meta.log_messages
+
+ def _delivery_pending(self, receipt: Receipt, signature: str) -> Receipt:
+ """Settle a confirmed signature as DELIVERY_PENDING, with the Mailbox message id when readable.
+
+ The logs supply nothing but the message id, and ``messageIdUnavailable`` already covers a missing
+ dispatch line, so a failing ``getTransaction`` degrades to that fallback instead of turning a
+ transfer that is already on-chain into a reported failure.
+ """
+ try:
+ logs = self._transaction_logs(signature)
+ except Exception: # noqa: BLE001 — RPC or decode failure reading the logs
+ logs = None
+ message_id = sl.extract_hyperlane_message_id(logs)
+ state = dict(receipt.protocol_state)
+ if message_id:
+ state["messageId"] = message_id
+ else:
+ state["messageIdUnavailable"] = True
+ return receipt.replace(id=message_id or signature, status=Status.DELIVERY_PENDING, protocol_state=state)
+
+ def _checkpoint(self, plan: Plan, receipt: Receipt, on_checkpoint: "Callable[[Checkpoint], None] | None",
+ store: "CheckpointStore | None", signature: str) -> None:
+ """Emit the checkpoint for a just-broadcast *signature* to the caller first, then the store.
+
+ Mirrors ``EvmCall._checkpoint``: the caller's callback runs before the store because the
+ transaction is already on the wire; a store failure is then fatal and names the signature,
+ because losing it silently would strand funds.
+ """
+ from .checkpoint import create_checkpoint
+
+ checkpoint = create_checkpoint(plan, receipt, self.registry)
+ if on_checkpoint is not None:
+ on_checkpoint(checkpoint) # the caller's own callback: errors are theirs
+ if store is not None:
+ try:
+ store.save(checkpoint)
+ except Exception as exc: # noqa: BLE001 — any store backend failure
+ raise BridgeError(
+ f"Solana transaction {signature} WAS broadcast but its checkpoint {checkpoint.id} could not be "
+ f"saved ({exc}); record the signature before retrying — resending would double-spend") from exc
+
+ def _submit(self, built: SolBuild, *, wait: bool, timeout_seconds: float, poll_seconds: float,
+ on_checkpoint: "Callable[[Checkpoint], None] | None" = None,
+ store: "CheckpointStore | None" = None,
+ on_broadcast: "Callable[[str], None] | None" = None) -> Receipt:
+ libs = _libs()
+ quote = built.quote
+ balance = self._balance_of(built.sender)
+ if balance < quote.total_lamports:
+ raise InsufficientBalanceError(
+ f"Insufficient Solana balance for this Hyperlane transfer: balance {balance} lamports, "
+ f"required {quote.total_lamports} lamports (amount {quote.plan.amount_atomic} "
+ f"+ gas {quote.igp_lamports + quote.network_fee_lamports} + rent {quote.rent_lamports})")
+ signatures = list(built.transaction.signatures)
+ payer_index = list(built.message.account_keys).index(libs.Pubkey.from_string(built.sender))
+ signatures[payer_index] = self.conn.sign_message(libs.to_bytes_versioned(built.message))
+ signed = libs.VersionedTransaction.populate(built.message, signatures)
+ opts = SendOptions(skip_preflight=False, preflight_commitment=CONFIRMED)
+ # A Solana transaction's id IS its fee-payer signature, so it is known before the send. Capture it
+ # first: if the RPC answer is lost the bytes may still have reached the cluster, and a caller who
+ # never learns the signature cannot tell a failed send from a landed one (and would resend).
+ signature = str(signed.signatures[0])
+ try:
+ response = self.client.send_raw_transaction(bytes(signed), opts=opts)
+ except Exception as exc: # noqa: BLE001 — any transport/decoding failure loses the response, not the send
+ if on_broadcast is not None:
+ on_broadcast(signature) # it may be on the wire: never let the caller resend
+ raise BridgeError(
+ f"Solana transaction {signature} may have been broadcast; the RPC response was lost: {exc}"
+ " — check bridge.sol.source_status / the explorer before retrying") from exc
+ if on_broadcast is not None:
+ on_broadcast(signature)
+ echoed = str(response.value)
+ if echoed != signature:
+ raise BridgeError(
+ f"Solana RPC echoed signature {echoed} for a transaction signed as {signature}; refusing to "
+ "checkpoint or follow the wrong id — check bridge.sol.source_status / the explorer for "
+ f"{signature} before retrying")
+ receipt = self._source_receipt(built, signature)
+ self._checkpoint(quote.plan, receipt, on_checkpoint, store, signature)
+ if not wait:
+ return receipt
+ try:
+ outcome = _poll_for_confirmation(self.client, signature, built.blockhash, timeout_seconds, poll_seconds)
+ if outcome is None:
+ return receipt
+ if outcome == "expired":
+ return receipt.replace(status=Status.EXPIRED, protocol_state={
+ **receipt.protocol_state, "blockhashExpired": True,
+ "sourceError": f"Solana transaction expired before confirmation: {signature}"})
+ return self._delivery_pending(receipt, signature)
+ except BridgeError as exc:
+ if signature in str(exc):
+ raise
+ raise BridgeError(f"Solana Hyperlane transfer {signature} failed after broadcast: {exc}") from exc
+ except Exception as exc: # noqa: BLE001 — any post-broadcast failure names the signature
+ raise BridgeError(f"Solana Hyperlane transfer {signature} failed after broadcast: {exc}") from exc
+
+ # --- status -----------------------------------------------------------------------------
+
+ def source_status(self, plan: Plan, receipt: Receipt) -> Receipt:
+ """One refresh of a SOURCE_CONFIRMING Solana receipt (veil ``getSourceStatus``): unknown → unchanged, or
+ EXPIRED once the checkpointed blockhash is invalid; processed → unchanged; failed → raises;
+ confirmed/finalized → DELIVERY_PENDING with the Mailbox message id when the log is available."""
+ if receipt.protocol != "hyperlane" or receipt.status is not Status.SOURCE_CONFIRMING or not receipt.source_tx_id:
+ raise BridgeError("Solana Hyperlane source status requires a source-confirming Hyperlane receipt with a signature")
+ if receipt.protocol_state.get("routeId") != plan.route_id:
+ raise BridgeError(f"receipt route {receipt.protocol_state.get('routeId')} does not match plan route {plan.route_id}")
+ libs = _libs()
+ signature = receipt.source_tx_id
+ status = _signature_status(self.client, libs.Signature.from_string(signature))
+ if status is None:
+ blockhash = receipt.protocol_state.get("blockhash")
+ height = receipt.protocol_state.get("lastValidBlockHeight")
+ if blockhash is None and height is None:
+ return receipt
+ if not isinstance(blockhash, str) or not blockhash or not isinstance(height, str) or not height.isdigit():
+ raise CheckpointInvalidError("Solana Hyperlane source receipt has an invalid blockhash lifetime")
+ try:
+ hash_ = libs.Hash.from_string(blockhash)
+ except Exception as exc:
+ raise CheckpointInvalidError("Solana Hyperlane source receipt has an invalid blockhash lifetime") from exc
+ try:
+ valid = bool(self.client.is_blockhash_valid(hash_, commitment=CONFIRMED).value)
+ except Exception:
+ return receipt # advisory read; keep waiting
+ if not valid:
+ return receipt.replace(status=Status.EXPIRED, protocol_state={
+ **receipt.protocol_state, "blockhashExpired": True,
+ "sourceError": f"Solana transaction expired before confirmation: {signature}"})
+ return receipt
+ if status == "processed":
+ return receipt
+ if status == "failed":
+ raise BridgeError(f"Solana Hyperlane transfer failed on-chain: {signature}")
+ return self._delivery_pending(receipt, signature)
diff --git a/bridge-sdk/python/aleo_bridge/types.py b/bridge-sdk/python/aleo_bridge/types.py
new file mode 100644
index 0000000..4cb2203
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/types.py
@@ -0,0 +1,271 @@
+"""Typed results shared by every module (contract §types.py). Atomic amounts are ``int``; human amounts are ``str``."""
+from __future__ import annotations
+
+import dataclasses
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any
+
+from .errors import ConfigurationError
+
+
+class Status(str, Enum):
+ PREPARED = "PREPARED"
+ SOURCE_APPROVAL_PENDING = "SOURCE_APPROVAL_PENDING"
+ SOURCE_SUBMISSION_PENDING = "SOURCE_SUBMISSION_PENDING"
+ SOURCE_CONFIRMING = "SOURCE_CONFIRMING"
+ ATTESTATION_PENDING = "ATTESTATION_PENDING"
+ DESTINATION_ACTION_REQUIRED = "DESTINATION_ACTION_REQUIRED"
+ DELIVERY_PENDING = "DELIVERY_PENDING"
+ DESTINATION_CONFIRMING = "DESTINATION_CONFIRMING"
+ COMPLETED = "COMPLETED"
+ FAILED = "FAILED"
+ EXPIRED = "EXPIRED"
+
+ def __str__(self) -> str: # json.dumps and f-strings print the bare name
+ return self.value
+
+
+TERMINAL = {Status.COMPLETED, Status.FAILED, Status.EXPIRED}
+CALLER_BOUNDARIES = {Status.SOURCE_SUBMISSION_PENDING, Status.DESTINATION_ACTION_REQUIRED,
+ Status.COMPLETED, Status.FAILED, Status.EXPIRED}
+
+# brief §2.5
+_NEXT_BY_STATUS = {
+ Status.SOURCE_SUBMISSION_PENDING: "resume",
+ Status.DESTINATION_ACTION_REQUIRED: "complete",
+ Status.COMPLETED: "done",
+ Status.FAILED: "failed",
+ Status.EXPIRED: "failed",
+}
+
+
+@dataclass(frozen=True)
+class Step:
+ id: str
+ kind: str # approve|deposit|burn|dispatch|wait-attestation|mint|withdraw|wait-delivery|confirm-delivery
+ executor: str # aleo-wallet|evm-wallet|solana-wallet|protocol
+ irreversible: bool
+
+
+@dataclass(frozen=True)
+class Fee:
+ kind: str
+ chain_id: str
+ asset_id: str
+ amount: str
+ estimated: bool
+
+
+@dataclass(frozen=True)
+class Plan:
+ route_id: str
+ registry_version: str
+ protocol: str
+ environment: str
+ source_asset_id: str
+ destination_asset_id: str
+ amount: str
+ amount_atomic: int
+ recipient: str
+ sender: str | None
+ mint_mode: str # "public" | "record" | "private"
+ steps: tuple[Step, ...]
+
+ def to_dict(self) -> dict[str, Any]:
+ d = dataclasses.asdict(self)
+ d["steps"] = [dataclasses.asdict(s) for s in self.steps]
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict[str, Any]) -> "Plan":
+ data = dict(d)
+ data["steps"] = tuple(Step(**s) for s in data.get("steps", ()))
+ return cls(**data)
+
+
+@dataclass(frozen=True)
+class Quote:
+ kind: str # evm-hyperlane | solana-hyperlane | aleo-hyperlane | evm-xreserve | aleo-xreserve
+ plan: Plan
+ fees: tuple[Fee, ...]
+ amount_out: str | None
+
+
+@dataclass(frozen=True)
+class EvmHyperlaneQuote(Quote):
+ recipient_bytes32: bytes
+ native_value_atomic: int
+ native_fee_atomic: int
+ approval_required: bool | None
+
+
+@dataclass(frozen=True)
+class SolanaHyperlaneQuote(Quote):
+ igp_lamports: int
+ network_fee_lamports: int
+ rent_lamports: int
+ total_lamports: int
+ unique_message_address: str
+
+
+@dataclass(frozen=True)
+class AleoHyperlaneQuote(Quote):
+ gas_limit: int
+ gas_overhead: int
+ gas_price: int
+ exchange_rate: int
+ payment_microcredits: int
+
+
+@dataclass(frozen=True)
+class EvmXReserveQuote(Quote):
+ hook_data: bytes
+ remote_recipient_bytes32: bytes
+ balance_atomic: int
+ allowance_atomic: int
+ approval_required: bool
+ max_fee_atomic: int
+
+
+@dataclass(frozen=True)
+class AleoXReserveQuote(Quote):
+ withdrawal_fee_atomic: int
+
+
+@dataclass
+class Receipt:
+ id: str
+ protocol: str
+ status: Status
+ source_tx_id: str | None = None
+ destination_tx_id: str | None = None
+ protocol_state: dict[str, Any] = field(default_factory=dict) # MUST include "routeId"
+ next_action: dict[str, Any] | None = None # {"kind": "xreserve-private-mint", "chainId": ...}
+
+ def __post_init__(self) -> None:
+ self.status = Status(self.status)
+
+ def replace(self, **changes: Any) -> "Receipt":
+ # Copy the mutable fields (never alias self's dicts) so mutating the returned Receipt
+ # can never reach back into the one it was derived from.
+ changes.setdefault("protocol_state", dict(self.protocol_state))
+ changes.setdefault("next_action", None if self.next_action is None else dict(self.next_action))
+ return dataclasses.replace(self, **changes)
+
+
+@dataclass(frozen=True)
+class Progress:
+ next: str # "wait" | "resume" | "complete" | "done" | "failed"
+ plan: Plan
+ receipt: Receipt
+ error: str | None = None
+
+
+def to_progress(plan: Plan, receipt: Receipt) -> Progress:
+ """brief §2.5: SOURCE_SUBMISSION_PENDING→resume, DESTINATION_ACTION_REQUIRED→complete, COMPLETED→done,
+ FAILED|EXPIRED→failed, everything else→wait. For FAILED|EXPIRED, derive error from protocol_state."""
+ if "routeId" not in receipt.protocol_state:
+ raise ConfigurationError("Receipt.protocol_state must carry routeId")
+ status = Status(receipt.status)
+ next_val = _NEXT_BY_STATUS.get(status, "wait")
+
+ # Derive error for terminal failure states
+ error = None
+ if status in {Status.FAILED, Status.EXPIRED}:
+ error = (receipt.protocol_state.get("destinationError") or
+ receipt.protocol_state.get("sourceError") or
+ f"Bridge transfer ended in {status.value}")
+
+ return Progress(next_val, plan, receipt, error=error)
+
+
+@dataclass(frozen=True)
+class GasQuote:
+ route_id: str
+ gas_limit: int
+ gas_overhead: int
+ gas_price: int
+ exchange_rate: int
+ payment_microcredits: int
+
+
+@dataclass(frozen=True)
+class Attestation:
+ payload: bytes # 305 bytes
+ message_hash: bytes # 32 bytes
+ attestation: bytes # 65 bytes
+ status: str # "complete"
+
+
+@dataclass(frozen=True)
+class DispatchReceipt:
+ transaction_id: str
+ route_id: str
+ message_id: str | None
+ amount_atomic: int
+ receipt: Receipt
+
+
+@dataclass(frozen=True)
+class BurnReceipt:
+ transaction_id: str
+ route_id: str
+ mode: str
+ amount_atomic: int
+ receipt: Receipt
+
+
+@dataclass(frozen=True)
+class MintReceipt:
+ transaction_id: str
+ route_id: str
+ receipt: Receipt
+
+
+@dataclass(frozen=True)
+class DepositReceipt:
+ transaction_id: str
+ route_id: str
+ message_hash: str
+ nonce: str
+ receipt: Receipt
+
+
+@dataclass(frozen=True)
+class PrivacyReceipt:
+ transaction_id: str
+ asset_id: str
+ amount: str
+ amount_atomic: int
+ direction: str # "shield" | "unshield"
+
+
+@dataclass(frozen=True)
+class PreparedTx:
+ transaction_id: str
+ serialized: str # Transaction JSON; rebroadcast via network.submit_transaction(serialized)
+
+
+@dataclass
+class ChainStatus:
+ chain_id: str
+ address: str | None
+ can_sign: bool
+ balances: dict[str, int] # asset_id -> atomic
+
+
+@dataclass
+class BridgeStatus:
+ environment: str
+ registry_version: str
+ chains: list[ChainStatus]
+ pending: list[Progress]
+
+
+__all__ = [
+ "CALLER_BOUNDARIES", "TERMINAL", "AleoHyperlaneQuote", "AleoXReserveQuote", "Attestation", "BridgeStatus",
+ "BurnReceipt", "ChainStatus", "DepositReceipt", "DispatchReceipt", "EvmHyperlaneQuote", "EvmXReserveQuote",
+ "Fee", "GasQuote", "MintReceipt", "Plan", "PreparedTx", "PrivacyReceipt", "Progress", "Quote", "Receipt",
+ "SolanaHyperlaneQuote", "Status", "Step", "to_progress",
+]
diff --git a/bridge-sdk/python/aleo_bridge/units.py b/bridge-sdk/python/aleo_bridge/units.py
new file mode 100644
index 0000000..fc276ed
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/units.py
@@ -0,0 +1,60 @@
+"""Exact decimal ↔ atomic conversion (port of veil ``utils/units.ts``). No floats anywhere."""
+from __future__ import annotations
+
+import re
+from decimal import Decimal
+
+from .errors import InvalidAmountError
+
+_DECIMAL_RE = re.compile(r"^([0-9]+)(?:\.([0-9]+))?$")
+
+
+def _check_decimals(decimals: int) -> None:
+ if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0:
+ raise InvalidAmountError(f"Asset decimals must be a non-negative integer, got {decimals!r}")
+
+
+def parse_decimal_amount(amount: "str | Decimal | int", decimals: int) -> int:
+ """``"0.5"`` with 6 decimals → ``500000``. Strict: digits with an optional fraction only.
+
+ Rejects exponents, signs, a trailing dot, the empty string, floats, and any fraction longer
+ than *decimals* (that precision cannot exist on chain).
+ """
+ _check_decimals(decimals)
+ if isinstance(amount, bool) or not isinstance(amount, (str, int, Decimal)):
+ raise InvalidAmountError(f"Amount must be a decimal string, int or Decimal, got {type(amount).__name__}")
+ text = format(amount, "f") if isinstance(amount, Decimal) else str(amount).strip()
+ match = _DECIMAL_RE.match(text)
+ if not match:
+ raise InvalidAmountError(f'Invalid decimal amount "{amount}" — use digits with an optional fraction, e.g. "0.5"')
+ whole, frac = match.group(1), match.group(2) or ""
+ if len(frac) > decimals:
+ raise InvalidAmountError(
+ f'Amount "{amount}" has {len(frac)} fractional digits but the asset supports {decimals}')
+ return int(whole + frac.ljust(decimals, "0"))
+
+
+def format_decimal_amount(atomic: int, decimals: int) -> str:
+ """``2000001`` with 6 decimals → ``"2.000001"``; trailing fractional zeros are stripped."""
+ _check_decimals(decimals)
+ if isinstance(atomic, bool) or not isinstance(atomic, int) or atomic < 0:
+ raise InvalidAmountError(f"Atomic amount must be a non-negative int, got {atomic!r}")
+ if decimals == 0:
+ return str(atomic)
+ digits = str(atomic).rjust(decimals + 1, "0")
+ whole, fraction = digits[:-decimals], digits[-decimals:].rstrip("0")
+ return f"{whole}.{fraction}" if fraction else whole
+
+
+def resolve_amount(*, amount: "str | Decimal | int | None", amount_atomic: "int | None", decimals: int) -> int:
+ """Exactly one of *amount* (human) / *amount_atomic* (int) → atomic int."""
+ if (amount is None) == (amount_atomic is None):
+ raise InvalidAmountError("Pass exactly one of amount= (decimal string) or amount_atomic= (int)")
+ if amount_atomic is not None:
+ if isinstance(amount_atomic, bool) or not isinstance(amount_atomic, int) or amount_atomic < 0:
+ raise InvalidAmountError(f"amount_atomic must be a non-negative int, got {amount_atomic!r}")
+ return amount_atomic
+ return parse_decimal_amount(amount, decimals) # type: ignore[arg-type]
+
+
+__all__ = ["parse_decimal_amount", "format_decimal_amount", "resolve_amount"]
diff --git a/bridge-sdk/python/aleo_bridge/xreserve.py b/bridge-sdk/python/aleo_bridge/xreserve.py
new file mode 100644
index 0000000..c431891
--- /dev/null
+++ b/bridge-sdk/python/aleo_bridge/xreserve.py
@@ -0,0 +1,193 @@
+"""Aleo side of Circle xReserve (port of veil protocols/xreserve/aleoToEvm.ts, the ``complete`` half of
+evmToAleo.ts, and utils/xreserveDelivery.ts): USDCx burns, the user-signed private mint, attestation
+lookup and the ``nullifier`` delivery read."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+from . import encoding as enc
+from ._calls import AleoCall
+from ._keccak import keccak256
+from .circle import CircleClient
+from .errors import (AttestationError, ConfigurationError, InvalidAmountError, InvalidRecipientError,
+ RouteNotFoundError, RouteUnavailableError, UnsupportedRouteError)
+from .registry import Route
+from .types import Attestation, BurnReceipt, MintReceipt, Receipt, Status
+from .units import format_decimal_amount, resolve_amount
+
+if TYPE_CHECKING: # pragma: no cover
+ from .client import Bridge
+
+ETHEREUM_DESTINATION_DOMAIN = 0
+BURN_MODES = ("private", "public", "public-as-signer")
+
+
+class XReserveModule:
+ """``bridge.xreserve`` — every xReserve step that happens on Aleo."""
+
+ def __init__(self, bridge: "Bridge") -> None:
+ self._bridge = bridge
+ self.circle_session: Any = None # injectable HTTP session (tests); None → requests.Session()
+
+ # ── routes ──
+ def _single(self, direction: str) -> Route:
+ registry, aleo = self._bridge.registry, self._bridge.aleo_chain().id
+ matches = [r for r in registry.routes(protocol="xreserve", include_unavailable=True, environment=self._bridge.environment)
+ if registry.asset(r.destination_asset_id if direction == "inbound" else r.source_asset_id).chain_id == aleo]
+ if not matches:
+ raise RouteNotFoundError(f"No {direction} xReserve route for {self._bridge.environment}")
+ if len(matches) > 1:
+ raise ConfigurationError(f"Several {direction} xReserve routes for {self._bridge.environment}: {[r.id for r in matches]}")
+ if not matches[0].active:
+ raise RouteUnavailableError(f"xReserve route is not executable: {matches[0].id}")
+ return matches[0]
+
+ def inbound_route(self) -> Route:
+ """Ethereum → Aleo (mint side) for this environment."""
+ return self._single("inbound")
+
+ def outbound_route(self) -> Route:
+ """Aleo → Ethereum (burn side) for this environment."""
+ return self._single("outbound")
+
+ def _validated(self, route: Route, *, direction: str) -> Route:
+ registry = self._bridge.registry
+ if route.protocol != "xreserve":
+ raise UnsupportedRouteError(f"Not an xReserve route: {route.id}")
+ if not route.active:
+ raise RouteUnavailableError(f"xReserve route is not executable: {route.id}")
+ source = registry.chain(registry.asset(route.source_asset_id).chain_id).family
+ destination = registry.chain(registry.asset(route.destination_asset_id).chain_id).family
+ if direction == "burn" and (source, destination) != ("aleo", "evm"):
+ raise UnsupportedRouteError(f"USDCx burn requires an Aleo-to-Ethereum route, got {route.id}")
+ if direction == "mint" and (source, destination) != ("evm", "aleo"):
+ raise UnsupportedRouteError(f"private_mint requires an Ethereum-to-Aleo route, got {route.id}")
+ if route.meta_int("ethereumDestinationDomain") != ETHEREUM_DESTINATION_DOMAIN:
+ raise ConfigurationError(f"xReserve Ethereum destination domain must be {ETHEREUM_DESTINATION_DOMAIN}: {route.id}")
+ return route
+
+ # ── burn ──
+ def build_burn_inputs(self, route: Route, *, mode: str, amount_atomic: int, recipient: str,
+ record: str | None, merkle_proof: str | None) -> tuple[str, str, list[str]]:
+ """``(program, function, inputs)`` for one USDCx burn (brief §3.4). Pure."""
+ if mode not in BURN_MODES:
+ raise ConfigurationError(f"Unsupported USDCx burn mode {mode!r}; expected one of {BURN_MODES}")
+ self._validated(route, direction="burn")
+ source = self._bridge.registry.asset(route.source_asset_id)
+ if amount_atomic <= 0:
+ raise InvalidAmountError("USDCx burn amount must be greater than zero")
+ fee = int(route.meta_str("withdrawalFeeAtomic"))
+ if amount_atomic <= fee:
+ raise InvalidAmountError(
+ f"USDCx burn amount must exceed the {format_decimal_amount(fee, source.decimals)} {source.symbol} withdrawal fee")
+ recipient32 = enc.evm_address_to_bytes32(recipient) # InvalidRecipientError
+ amount_lit, domain_lit, recipient_lit = f"{amount_atomic}u128", f"{ETHEREUM_DESTINATION_DOMAIN}u32", enc.u8_array_literal(recipient32)
+ if mode == "private":
+ if not isinstance(record, str) or not record.strip():
+ raise ConfigurationError(f"private_burn requires a USDCx Token record from {route.meta_str('remoteToken')}")
+ if not isinstance(merkle_proof, str) or not (merkle_proof.startswith("[") and merkle_proof.endswith("]")):
+ raise ConfigurationError("private_burn requires an encoded [MerkleProof; 2] Aleo literal")
+ return route.meta_str("wrapperProgram"), "private_burn", [record, amount_lit, domain_lit, recipient_lit, merkle_proof]
+ function = "burn_public" if mode == "public" else "burn_public_as_signer"
+ return route.meta_str("bridgeProgram"), function, [amount_lit, domain_lit, recipient_lit]
+
+ def burn(self, recipient: str, *, amount: Any = None, amount_atomic: int | None = None, mode: str = "private",
+ record: str | None = None, merkle_proof: str | None = None) -> AleoCall[BurnReceipt]:
+ """Burn USDCx for USDC on Ethereum. ``private`` (default) spends a Token record via the wrapper and needs a
+ freeze-list exclusion proof — both are resolved from chain state when not supplied. Minimum: more than
+ the 2 USDCx withdrawal fee. The Aleo burn-attestation service forwards accepted burns to Circle."""
+ if mode not in BURN_MODES:
+ raise ConfigurationError(f"Unsupported USDCx burn mode {mode!r}; expected one of {BURN_MODES}")
+ if mode != "private" and (record is not None or merkle_proof is not None):
+ raise ConfigurationError(
+ f"mode={mode!r} burns the public balance; record=/merkle_proof= only apply to mode='private'")
+ route = self._validated(self.outbound_route(), direction="burn")
+ source = self._bridge.registry.asset(route.source_asset_id)
+ atomic = resolve_amount(amount=amount, amount_atomic=amount_atomic, decimals=source.decimals)
+ if mode == "private":
+ token_program = route.meta_str("remoteToken")
+ if record is None:
+ privacy = getattr(self._bridge, "privacy", None)
+ if privacy is None:
+ raise ConfigurationError(
+ "burn(mode='private') needs a record; pass record= explicitly (bridge.privacy is not available yet)")
+ record = privacy.select_record(token_program, atomic)
+ if merkle_proof is None:
+ freezelist = getattr(self._bridge, "freezelist", None)
+ if freezelist is None:
+ raise ConfigurationError(
+ "burn(mode='private') needs merkle_proof; pass merkle_proof= explicitly "
+ "(bridge.freezelist is not available yet)")
+ merkle_proof = freezelist.exclusion_proof(self._bridge.aleo_address(), token_program)
+ program, function, inputs = self.build_burn_inputs(route, mode=mode, amount_atomic=atomic, recipient=recipient,
+ record=record, merkle_proof=merkle_proof)
+ recipient_hex = enc.to_hex(enc.evm_address_to_bytes32(recipient))
+
+ def build(tx_id: str, _outputs: list[str]) -> BurnReceipt:
+ receipt = Receipt(id=tx_id, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id,
+ protocol_state={"routeId": route.id, "burnMode": mode, "amountAtomic": str(atomic),
+ "nativeDomain": ETHEREUM_DESTINATION_DOMAIN, "nativeRecipientBytes32": recipient_hex,
+ "sourceProgram": program, "sourceFunction": function,
+ "forwardingService": "aleo-burn-attestation"})
+ return BurnReceipt(transaction_id=tx_id, route_id=route.id, mode=mode, amount_atomic=atomic, receipt=receipt)
+
+ return self._bridge._call(program, function, inputs, build)
+
+ # ── private mint ──
+ def hook_data(self, mode: str, recipient: str, secret_nonce: str = "0scalar") -> bytes:
+ return enc.xreserve_hook_data(mode, recipient, self._bridge.environment, secret_nonce)
+
+ def build_private_mint_inputs(self, route: Route, attestation: Attestation, recipient: str, secret_nonce: str) -> list[str]:
+ """The five ``private_mint`` literals (brief §3.5) after re-verifying hash and the (recipient, nonce) commitment."""
+ self._validated(route, direction="mint")
+ enc.validate_scalar(secret_nonce)
+ if attestation.status != "complete":
+ raise AttestationError("Private mint requires a completed Circle attestation; it is still pending")
+ payload, signature, digest = bytes(attestation.payload), bytes(attestation.attestation), bytes(attestation.message_hash)
+ if len(payload) != enc.XRESERVE_PAYLOAD_BYTES or len(signature) != enc.HOOK_DATA_BYTES or len(digest) != 32:
+ raise AttestationError("Circle attestation has invalid widths (expected 305-byte payload, 65-byte signature, 32-byte hash)")
+ if keccak256(payload) != digest:
+ raise AttestationError("Circle attestation payload has an invalid message hash")
+ expected_hook = self.hook_data("private", recipient, secret_nonce)
+ if payload[-enc.HOOK_DATA_BYTES:] != expected_hook:
+ raise AttestationError("Private mint secret nonce and recipient do not match the attested hook data")
+ return [enc.u8_array_literal(payload), enc.u8_array_literal(signature), enc.u8_array_literal(digest), secret_nonce, recipient]
+
+ def private_mint(self, attestation: Attestation, recipient: str, *, secret_nonce: str = "0scalar",
+ route: Route | None = None) -> AleoCall[MintReceipt]:
+ """Finish a private-mode deposit: the only user-signed Aleo step of the inbound flow (``wrapper.private_mint``)."""
+ route = self._validated(route if route is not None else self.inbound_route(), direction="mint")
+ inputs = self.build_private_mint_inputs(route, attestation, recipient, secret_nonce)
+ program = route.meta_str("wrapperProgram")
+ message_hash = enc.to_hex(attestation.message_hash)
+ nonce = enc.to_hex(enc.xreserve_nonce_from_payload(attestation.payload))
+
+ def build(tx_id: str, _outputs: list[str]) -> MintReceipt:
+ receipt = Receipt(id=message_hash, protocol="xreserve", status=Status.DESTINATION_CONFIRMING, destination_tx_id=tx_id,
+ protocol_state={"routeId": route.id, "mintMode": "private", "intendedRecipient": recipient,
+ "messageHash": message_hash, "nonce": nonce,
+ "bridgeProgram": route.meta_str("bridgeProgram"), "wrapperProgram": program,
+ "destinationProgram": program, "destinationFunction": "private_mint"})
+ return MintReceipt(transaction_id=tx_id, route_id=route.id, receipt=receipt)
+
+ return self._bridge._call(program, "private_mint", inputs, build)
+
+ # ── reads ──
+ def get_attestation(self, message_hash: "str | bytes", *, route: Route | None = None) -> Attestation | None:
+ """One Circle request for *message_hash*; ``None`` while pending (404)."""
+ route = route if route is not None else self.inbound_route()
+ client = CircleClient(route.meta_str("attestationBaseUrl"), session=self.circle_session)
+ return client.get_attestation(enc.to_hex(enc.hex_to_bytes(message_hash)))
+
+ def is_delivered(self, nonce: "str | bytes", *, route: Route | None = None) -> bool:
+ """``bridgeProgram/nullifier[nonce as [u8; 32]] == true`` — the mint already landed on Aleo."""
+ route = route if route is not None else self.inbound_route()
+ try:
+ raw = enc.hex_to_bytes(nonce, 32)
+ except (ValueError, InvalidRecipientError) as exc:
+ raise ConfigurationError("xReserve delivery requires a 32-byte deposit nonce") from exc
+ value = self._bridge.mapping_value(route.meta_str("bridgeProgram"), "nullifier", enc.u8_array_literal(raw))
+ return value is not None and value.strip() == "true"
+
+
+__all__ = ["BURN_MODES", "ETHEREUM_DESTINATION_DOMAIN", "XReserveModule"]
diff --git a/bridge-sdk/scripts/rehearse.py b/bridge-sdk/scripts/rehearse.py
new file mode 100644
index 0000000..269aba2
--- /dev/null
+++ b/bridge-sdk/scripts/rehearse.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3
+"""Operator front-end over the live cases in ``tests/live/cases.py``.
+
+ python scripts/rehearse.py --case evm-hyperlane --quote-only # price every route, submit nothing
+ python scripts/rehearse.py --case evm-hyperlane --route hyperlane:ethereum/eth->aleo/eth
+ python scripts/rehearse.py --recover # continue one interrupted transfer
+ python scripts/rehearse.py --case solana-hyperlane --report run.json
+
+The CLI and the pytest suite call the SAME case functions, so a rehearsal and a test cannot drift.
+
+This script never sets, exports, prints or suggests a value for an acknowledgement variable. It
+READS ``BRIDGE_LIVE_FUNDS`` / ``BRIDGE_LIVE_STATE_DIR`` / ``BRIDGE_LIVE_MAINNET_ACK`` /
+``BRIDGE_LIVE_MAINNET_CASES`` / ``BRIDGE_LIVE_MAINNET_EXECUTE``; without all of them it runs to the
+quote and reports what it would have submitted. Keys, secret nonces, attestations and record
+plaintexts never reach the output — addresses, amounts and transaction ids do.
+
+Exit codes: ``0`` everything ran (or was quoted/skipped), ``1`` a case failed, ``2`` a case is
+still pending (a timeout leaves the checkpoint on disk; re-run with ``--recover``).
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Callable, Iterable
+
+ROOT = Path(__file__).resolve().parents[1]
+if str(ROOT) not in sys.path:
+ sys.path.insert(0, str(ROOT))
+if str(ROOT / "python") not in sys.path:
+ sys.path.insert(0, str(ROOT / "python"))
+
+from aleo_bridge.errors import BridgeError, PollingTimeoutError # noqa: E402
+from tests.live import cases as live_cases # noqa: E402
+from tests.live import config as live_config # noqa: E402
+from tests.live.helpers import (LiveBenchmark, LiveCaseError, LiveTimeoutError, # noqa: E402
+ Underfunded, load_live_state)
+
+EXIT_OK = 0
+EXIT_FAILED = 1
+EXIT_PENDING = 2
+
+_COLUMNS = (("case", 16), ("route_id", 40), ("status", 11), ("source_tx_id", 18), ("reason", 40))
+
+
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ prog="rehearse.py", description="Run one live bridge case over its registry routes.",
+ epilog="Mainnet submission additionally requires the acknowledgement variables described "
+ "in the README's 'Live tests' section; this script only reads them.")
+ parser.add_argument("--case", choices=list(live_cases.CASE_NAMES),
+ help="the veil case to run (omit only with --recover)")
+ parser.add_argument("--route", help="restrict the run to one registry route id")
+ parser.add_argument("--quote-only", action="store_true",
+ help="price and preflight every route; never submit, whatever is acknowledged")
+ parser.add_argument("--recover", metavar="STATE.json",
+ help="continue the transfer recorded in one state file (its route picks the case)")
+ parser.add_argument("--report", metavar="PATH", help="write the JSON report here")
+ args = parser.parse_args(argv)
+ if not args.case and not args.recover:
+ parser.error("give --case NAME or --recover STATE.json")
+ return args
+
+
+# ── target selection ──────────────────────────────────────────────────────────
+
+def resolve_routes(registry: Any, case: str, route: str | None = None,
+ environment: str = "mainnet") -> list[Any]:
+ """The routes this invocation covers: every route of *case*, or the single one asked for."""
+ routes = live_cases.routes_for_case(registry, case, environment)
+ if route is None:
+ return routes
+ chosen = [candidate for candidate in routes if candidate.id == route]
+ if not chosen:
+ raise SystemExit(f"Route {route} is not one of the {case} routes for {environment}")
+ return chosen
+
+
+def execution_allowed(case: str, *, quote_only: bool) -> tuple[bool, str]:
+ """Whether the wallet may submit, and the reason when it may not (variable names only)."""
+ if quote_only:
+ return False, "--quote-only was given"
+ if not live_config.live_funds_enabled():
+ return False, (f"{live_config.FUNDS_VAR} and {live_config.STATE_DIR_VAR} do not enable "
+ "funded live cases")
+ if not live_config.mainnet_case_enabled(case):
+ return False, (f"{live_config.MAINNET_ACK_VAR} and {live_config.MAINNET_CASES_VAR} do not "
+ f"enable the {case} case")
+ if not live_config.mainnet_execution_enabled():
+ return False, f"{live_config.MAINNET_EXECUTE_VAR} does not acknowledge mainnet submission"
+ return True, "acknowledged"
+
+
+def state_path_for(case: str, route_id: str, environment: str) -> Path:
+ return live_config.live_state_path(environment, live_cases.state_name(case, route_id))
+
+
+def resume_command(state_path: Path | str) -> str:
+ return f"python scripts/rehearse.py --recover {state_path}"
+
+
+# ── running ───────────────────────────────────────────────────────────────────
+
+def _row(case: str, route_id: str, status: str, reason: str = "", state: Any = None,
+ state_path: Path | str | None = None, benchmark: LiveBenchmark | None = None) -> dict[str, Any]:
+ return {
+ "case": case,
+ "route_id": route_id,
+ "status": status,
+ "reason": reason,
+ "source_tx_id": getattr(state, "source_tx_id", None),
+ "message_id": getattr(state, "message_id", None),
+ "destination_tx_id": getattr(state, "destination_tx_id", None),
+ "state_path": str(state_path) if state_path else None,
+ "resume": resume_command(state_path) if state_path else "",
+ "benchmark": benchmark.as_dict() if benchmark is not None else {},
+ }
+
+
+def run_route(bridge: Any, case: str, route: Any, *, execute: bool, state_path: Path,
+ log: Callable[[str], None] = print) -> dict[str, Any]:
+ """One route: run the case, and turn every outcome into a report row rather than a traceback."""
+ if not route.active:
+ return _row(case, route.id, "skipped", f"registry availability: {route.availability}")
+ benchmark = LiveBenchmark(f"{case}:{live_cases.route_slug(route.id)}", log=log)
+ runner = live_cases.RUNNERS[case]
+ try:
+ state = runner(bridge, route.id, state_path=state_path, execute=execute, benchmark=benchmark,
+ log=log)
+ except Underfunded as exc:
+ return _row(case, route.id, "skipped", str(exc), state_path=state_path, benchmark=benchmark)
+ except (PollingTimeoutError, LiveTimeoutError) as exc:
+ return _row(case, route.id, "pending", f"still in flight: {exc}", state_path=state_path,
+ benchmark=benchmark)
+ except (LiveCaseError, BridgeError) as exc:
+ return _row(case, route.id, "failed", f"{type(exc).__name__}: {exc}", state_path=state_path,
+ benchmark=benchmark)
+ status = "completed" if state.completed else ("quote-only" if not execute else "pending")
+ reason = "" if status != "pending" else "the transfer has not reached done yet"
+ return _row(case, route.id, status, reason, state=state, state_path=state_path, benchmark=benchmark)
+
+
+def _cell(value: Any, width: int) -> str:
+ text = str(value or "")
+ return text.ljust(width) if len(text) <= width else text[:width - 1] + "…"
+
+
+def render_table(rows: Iterable[dict[str, Any]]) -> str:
+ """A fixed-width table of what ran, was skipped, is pending, or failed."""
+ header = " ".join(name.replace("_", " ").upper().ljust(width) for name, width in _COLUMNS)
+ lines = [header, "-" * len(header)]
+ lines.extend(" ".join(_cell(row.get(name), width) for name, width in _COLUMNS) for row in rows)
+ return "\n".join(lines) + "\n"
+
+
+def exit_code(rows: list[dict[str, Any]]) -> int:
+ statuses = {row["status"] for row in rows}
+ if "failed" in statuses:
+ return EXIT_FAILED
+ if "pending" in statuses:
+ return EXIT_PENDING
+ return EXIT_OK
+
+
+def _default_bridge() -> Any:
+ from aleo_bridge import Bridge
+
+ # Bridge.from_env() already reads BRIDGE_LIVE_ETHEREUM_RPC_URL / BRIDGE_LIVE_SOLANA_RPC_URL as
+ # aliases of ETHEREUM_RPC_URL / SOLANA_RPC_URL, so veil's shell works unchanged.
+ return Bridge.from_env()
+
+
+def run(argv: list[str] | None = None, *, bridge_factory: Callable[[], Any] = _default_bridge,
+ log: Callable[[str], None] = print) -> int:
+ args = parse_args(argv)
+ run_benchmark = LiveBenchmark("rehearse", log=log)
+ bridge = bridge_factory()
+ run_benchmark.mark("clients-created")
+ environment = bridge.environment
+
+ if args.recover:
+ state_path = Path(args.recover).expanduser().resolve()
+ route_id = json.loads(state_path.read_text(encoding="utf-8")).get("routeId")
+ if not isinstance(route_id, str):
+ raise SystemExit(f"{state_path} does not name a routeId")
+ route = bridge.registry.route(route_id)
+ case = args.case or live_cases.case_for_route(bridge.registry, route)
+ if case is None:
+ raise SystemExit(f"No live case covers {route_id}")
+ load_live_state(state_path, route_id) # fail closed before anything else
+ targets = [(route, state_path)]
+ else:
+ case = args.case
+ targets = [(route, state_path_for(case, route.id, environment))
+ for route in resolve_routes(bridge.registry, case, args.route, environment)]
+
+ execute, reason = execution_allowed(case, quote_only=args.quote_only)
+ log(f"case {case} · environment {environment} · registry {bridge.registry.version}")
+ log(f"submission: {'ENABLED' if execute else 'disabled'} ({reason})")
+ if not execute and not args.quote_only:
+ log("Nothing will be submitted. Mainnet submission is gated on the acknowledgement variables "
+ f"({live_config.MAINNET_ACK_VAR}, {live_config.MAINNET_CASES_VAR}, "
+ f"{live_config.MAINNET_EXECUTE_VAR}); set them yourself, for one command, in your own shell.")
+
+ rows = [run_route(bridge, case, route, execute=execute, state_path=path, log=log)
+ for route, path in targets]
+
+ log("")
+ log(render_table(rows))
+ for row in rows:
+ if row["status"] == "pending":
+ log(f"pending {row['route_id']}: {row['resume']}")
+
+ payload = {"generated_at": datetime.now(timezone.utc).isoformat(), "case": case,
+ "environment": environment, "registry_version": bridge.registry.version,
+ "execute": execute, "reason": reason, "results": rows,
+ "run_benchmark": run_benchmark.as_dict()}
+ if args.report:
+ report = Path(args.report).expanduser()
+ report.parent.mkdir(parents=True, exist_ok=True)
+ report.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ log(f"report written to {report}")
+ return exit_code(rows)
+
+
+def main() -> int:
+ return run()
+
+
+if __name__ == "__main__": # pragma: no cover — exercised through run()
+ raise SystemExit(main())
diff --git a/bridge-sdk/tests/__init__.py b/bridge-sdk/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bridge-sdk/tests/conftest.py b/bridge-sdk/tests/conftest.py
new file mode 100644
index 0000000..8161ba0
--- /dev/null
+++ b/bridge-sdk/tests/conftest.py
@@ -0,0 +1,252 @@
+"""Hermetic stand-ins for the aleo facade. Records every call so tests assert on exact inputs."""
+from __future__ import annotations
+
+import json
+from typing import Any
+
+import pytest
+
+SIGNER = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px"
+IGP = "aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74"
+IGP_KEY_ETH = f"{{ igp: {IGP}, destination: 1u32 }}"
+IGP_KEY_SOL = f"{{ igp: {IGP}, destination: 1399811149u32 }}"
+ETH_GAS_CONFIG = "{\n gas_overhead: 159337u128,\n exchange_rate: 402u128,\n gas_price: 1000000000u128\n}"
+SOL_GAS_CONFIG = "{ gas_overhead: 200000u128, exchange_rate: 1000u128, gas_price: 50000000u128 }"
+USDCX_RECORD = f"{{ owner: {SIGNER}.private, amount: 5000000u128.private, _nonce: 7group.public }}"
+USDCX_RECORD_SMALL = f"{{ owner: {SIGNER}.private, amount: 100u128.private, _nonce: 8group.public }}"
+DELIVERED_KEY = "{ id: [262854447642257427123071959211115528903u128, 102980212169860384794748804418278302317u128] }"
+NULLIFIED_NONCE = bytes.fromhex("aa" * 32)
+CHILD = {"program": "usdcx_stablecoin.aleo", "function": "transfer_private_to_public", "outputs": [{"value": "1field"}]}
+
+
+def _source(program_id: str) -> str:
+ return f"program {program_id};\nfunction main:\n input r0 as u64.public;\n output r0 as u64.public;\n"
+
+
+class FakeAccount:
+ def __init__(self, address: str = SIGNER) -> None:
+ self.address = address
+ self.private_key = "APrivateKey1zkpFake"
+
+
+class FakeMapping:
+ def __init__(self, values: dict[str, Any]) -> None:
+ self._values = values
+
+ def get(self, key: Any) -> Any:
+ return self._values.get(str(key)) # None ≙ absent/null mapping entry
+
+
+class FakeTransition:
+ def __init__(self, program: str, function: str, outputs: list) -> None:
+ self.program_id, self.function_name, self._outputs = program, function, outputs
+
+ def outputs(self) -> list:
+ return list(self._outputs)
+
+
+def _tx_json(tx_id: str, program: str, function: str, outputs: list) -> dict:
+ return {"id": tx_id, "type": "execute", "execution": {"transitions": [
+ dict(CHILD), {"program": program, "function": function, "outputs": outputs}]}}
+
+
+class FakeTx:
+ def __init__(self, tx_id: str, program: str, function: str, outputs: list) -> None:
+ self.id = tx_id
+ self._json = _tx_json(tx_id, program, function, outputs)
+ self.raw = json.dumps(self._json) # str(raw) is what the node accepts
+
+ def transitions(self) -> list[FakeTransition]:
+ return [FakeTransition(t["program"], t["function"], t["outputs"]) for t in self._json["execution"]["transitions"]]
+
+ def decoded(self) -> list[dict]:
+ return [{"program": t.program_id, "function": t.function_name, "outputs": t.outputs()} for t in self.transitions()]
+
+
+class FakeBound:
+ def __init__(self, aleo: "FakeAleo", program_id: str, function_name: str, args: tuple) -> None:
+ self._aleo, self.program_id, self.function_name = aleo, program_id, function_name
+ self.args = [str(a) for a in args]
+ aleo.calls.append((program_id, function_name, list(self.args)))
+
+ def simulate(self, account: Any = None) -> str:
+ self._aleo.simulated.append((self.program_id, self.function_name))
+ return "simulated"
+
+ def build_transaction(self, account: Any = None, **fee: Any) -> FakeTx:
+ self._aleo.fee_kwargs.append(dict(fee))
+ return FakeTx("at1built", self.program_id, self.function_name, [{"value": "77field"}])
+
+ def delegate(self, account: Any = None, *, broadcast: bool = True, **fee: Any) -> dict:
+ self._aleo.delegated.append({"program": self.program_id, "function": self.function_name, "broadcast": broadcast, **fee})
+ if self._aleo.delegate_returns_id_only:
+ return {"transaction_id": "at1delegated"}
+ return {"transaction": _tx_json("at1delegated", self.program_id, self.function_name, [{"value": "77field"}])}
+
+
+class FakeFunctions:
+ def __init__(self, aleo: "FakeAleo", program_id: str) -> None:
+ self._aleo, self._program_id = aleo, program_id
+
+ def __getitem__(self, name: str):
+ return lambda *args: FakeBound(self._aleo, self._program_id, name, args)
+
+ __getattr__ = __getitem__
+
+
+class FakeProgram:
+ def __init__(self, aleo: "FakeAleo", program_id: str) -> None:
+ self.id = program_id
+ self.source = _source(program_id)
+ self.imports = list(aleo.imports.get(program_id, []))
+ self.raw = ("raw", program_id)
+ self.functions = FakeFunctions(aleo, program_id)
+ self._mappings = aleo.mappings.get(program_id, {})
+
+ def mapping(self, name: str) -> FakeMapping:
+ return FakeMapping(self._mappings.get(name, {}))
+
+ def mappings(self) -> list[str]:
+ return sorted(self._mappings)
+
+
+class FakePrograms:
+ def __init__(self, aleo: "FakeAleo") -> None:
+ self._aleo = aleo
+
+ def get(self, program_id: str) -> FakeProgram:
+ if program_id in self._aleo.missing_programs:
+ from aleo.facade.errors import ProgramNotFound
+ raise ProgramNotFound(program_id)
+ self._aleo.fetched.append(program_id)
+ return FakeProgram(self._aleo, program_id)
+
+
+class FakeRecords:
+ def __init__(self, aleo: "FakeAleo") -> None:
+ self._aleo = aleo
+
+ def find(self, account: Any = None, *, program: str | None = None, record: str | None = None,
+ unspent: bool = True, **_: Any) -> list[dict]:
+ self._aleo.record_queries.append({"program": program, "record": record, "unspent": unspent})
+ return [dict(r) for r in self._aleo.record_rows if program is None or r.get("program") == program]
+
+
+class FakeNetwork:
+ def __init__(self, aleo: "FakeAleo") -> None:
+ self._aleo = aleo
+
+ def submit_transaction(self, transaction: Any) -> str:
+ self._aleo.submitted.append(transaction)
+ if self._aleo.duplicate_on_submit:
+ from aleo.facade.errors import AleoNetworkError
+ # Mirrors the real facade: a node's duplicate-broadcast rejection is an AleoNetworkError
+ # whose message contains "already exists" (matched by is_duplicate_submission), not a
+ # bare RuntimeError.
+ raise AleoNetworkError("Transaction 'at1prepared' already exists in the ledger")
+ if isinstance(transaction, str):
+ return str(json.loads(transaction)["id"])
+ return str(getattr(transaction, "id", "at1built"))
+
+ def wait_for_transaction(self, tx_id: str, *, timeout: float = 45.0, poll_interval: float = 2.0) -> dict:
+ self._aleo.waited.append((tx_id, timeout))
+ if self._aleo.wait_raises:
+ from aleo.facade.errors import TransactionConfirmationTimeout
+ raise TransactionConfirmationTimeout(tx_id, timeout)
+ return {"status": "accepted"}
+
+ def get_transaction_object(self, tx_id: str) -> FakeTx:
+ return FakeTx(tx_id, "hyp_warp_token_wbtc_v2.aleo", "transfer_remote", [{"value": "99field"}])
+
+ def get_confirmed_transaction(self, tx_id: str) -> Any:
+ """Script via ``FakeAleo.confirmed_transactions[tx_id] = ``; else raises TransactionNotFound
+ (mirrors the real facade's 404 mapping) — additive for lifecycle recovery/status tests."""
+ self._aleo.confirmed_transaction_queries.append(tx_id)
+ if tx_id in self._aleo.confirmed_transactions:
+ return self._aleo.confirmed_transactions[tx_id]
+ from aleo.facade.errors import TransactionNotFound
+ raise TransactionNotFound(tx_id)
+
+
+class FakeProcess:
+ def __init__(self, aleo: "FakeAleo") -> None:
+ self._aleo = aleo
+
+ def contains_program(self, program_id: Any) -> bool:
+ return str(program_id) in self._aleo.registered
+
+ def add_program(self, program: Any) -> None:
+ self._aleo.registered.append(str(program[1]) if isinstance(program, tuple) else str(program))
+
+
+class FakeAleo:
+ """Facade stand-in: mappings keyed program → mapping → key; records; network; process; recorders."""
+
+ def __init__(self, mappings: dict | None = None, records: list[dict] | None = None,
+ network_name: str = "mainnet", default_account: Any = None, imports: dict | None = None,
+ missing_programs: "set[str] | None" = None) -> None:
+ self.network_name = network_name
+ self.default_account = FakeAccount() if default_account is None else default_account
+ self.mappings = mappings or {}
+ # ``records`` is the module (aleo.records.find); the rows it returns live in ``record_rows``.
+ self.record_rows = records if records is not None else [{"program": "usdcx_stablecoin.aleo", "record_plaintext": USDCX_RECORD}]
+ self.imports = imports or {}
+ # program ids that raise ProgramNotFound from programs.get() instead of returning a FakeProgram.
+ self.missing_programs = set(missing_programs or ())
+ self.calls: list = []
+ self.simulated: list = []
+ self.delegated: list = []
+ self.fee_kwargs: list = []
+ self.submitted: list = []
+ self.waited: list = []
+ self.fetched: list = []
+ self.registered: list = []
+ self.record_queries: list = []
+ self.confirmed_transactions: dict[str, Any] = {} # tx id -> confirmed-transaction JSON (script for get_confirmed_transaction)
+ self.confirmed_transaction_queries: list = []
+ self.duplicate_on_submit = False
+ self.delegate_returns_id_only = False
+ self.wait_raises = False
+ self.programs = FakePrograms(self)
+ self.records = FakeRecords(self)
+ self.record_provider = self.records
+ self.network = FakeNetwork(self)
+ self.process = FakeProcess(self)
+
+
+class FakeNetModule:
+ """Stands in for aleo. inside AleoCall's import registration (no real parsing)."""
+
+ class Program:
+ @staticmethod
+ def from_source(source: str):
+ return ("raw", source.split(";")[0].removeprefix("program "))
+
+ class ProgramID:
+ @staticmethod
+ def from_string(value: str) -> str:
+ return value
+
+
+def default_mappings() -> dict:
+ return {
+ "hyp_hook_manager.aleo": {"destination_gas_configs": {IGP_KEY_ETH: ETH_GAS_CONFIG, IGP_KEY_SOL: SOL_GAS_CONFIG}},
+ "hyp_mailbox.aleo": {"deliveries": {DELIVERED_KEY: "{ block_height: 1u32 }"}},
+ "usdcx_bridge_v2.aleo": {"nullifier": {"[" + ",".join(f"{b}u8" for b in NULLIFIED_NONCE) + "]": "true"}},
+ "credits.aleo": {"account": {SIGNER: "2392443u64"}},
+ "usdcx_stablecoin.aleo": {"balances": {SIGNER: "1000000u128"}, "freeze_list": {}, "freeze_list_last_index": {}},
+ "arc20_wbtc.aleo": {"balances": {SIGNER: "10000u128"}},
+ "arc20_eth.aleo": {"balances": {}},
+ }
+
+
+@pytest.fixture
+def fake_aleo(monkeypatch) -> FakeAleo:
+ monkeypatch.setattr("aleo_bridge._calls._network_module", lambda aleo: FakeNetModule)
+ return FakeAleo(mappings=default_mappings())
+
+
+@pytest.fixture
+def bridge(fake_aleo):
+ from aleo_bridge import Bridge
+ return Bridge(fake_aleo)
diff --git a/bridge-sdk/tests/fakes/__init__.py b/bridge-sdk/tests/fakes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bridge-sdk/tests/fakes/fake_bridge.py b/bridge-sdk/tests/fakes/fake_bridge.py
new file mode 100644
index 0000000..23840c0
--- /dev/null
+++ b/bridge-sdk/tests/fakes/fake_bridge.py
@@ -0,0 +1,519 @@
+"""A duck-typed Bridge whose protocol modules record calls and return scripted results.
+
+Shape fidelity matters: results carry the real ``types`` dataclasses, Aleo calls
+expose ``delegate_prepared / prove / submit_prepared`` exactly like ``AleoCall``,
+EVM/Solana calls invoke ``on_checkpoint(receipt)`` for every intermediate
+submission before returning, and ``eth``/``sol`` are properties that raise
+``ConfigurationError`` when the matching ``ethereum``/``solana`` connection is
+``None`` — the conventions plans 2/3 implement.
+
+``FakeBridge.aleo`` is ``tests.conftest.FakeAleo`` (the facade-level fake already
+used by plan 1-3 tests) rather than a second, ad hoc Aleo fake: there is exactly
+one Aleo fake in this test suite.
+"""
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field, replace
+from typing import Any, Callable
+
+from aleo import AleoNetworkError
+from aleo.facade.errors import TransactionNotFound
+
+from aleo_bridge.checkpoint import Checkpoint, create_checkpoint
+from aleo_bridge.errors import (AttestationError, ConfigurationError, InvalidRecipientError,
+ RegistryVersionMismatchError)
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import (Attestation, BridgeStatus, BurnReceipt, ChainStatus, DepositReceipt,
+ DispatchReceipt, EvmHyperlaneQuote, EvmXReserveQuote, GasQuote,
+ MintReceipt, PreparedTx, PrivacyReceipt, Receipt,
+ SolanaHyperlaneQuote, Status)
+
+from tests.conftest import FakeAleo as _ConftestFakeAleo
+from tests.conftest import default_mappings
+
+ALEO_RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+#: What ``PrivacyModule.unshield`` really puts into ``call.inputs``: the selected record's
+#: PLAINTEXT (see ``privacy.py`` — ``select_record`` returns the plaintext string). Anything that
+#: renders a built call for a human or a model has to redact it.
+RECORD_PLAINTEXT = (
+ "{ owner: aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n.private, "
+ "amount: 100000000u128.private, "
+ "_nonce: 5749463759923163832671233077408835222301563867853163045949890371815825289938group.public }")
+EVM_ADDRESS = "0x0000000000000000000000000000000000000001"
+SOL_ADDRESS = "11111111111111111111111111111111"
+OUTBOUND = {"aleo/eth": "ethereum/eth", "aleo/wbtc": "ethereum/wbtc",
+ "aleo/usdt": "ethereum/usdt", "aleo/sol": "solana/sol"}
+
+
+def serialized_tx(tx_id: str) -> str:
+ return json.dumps({"type": "execute", "id": tx_id, "fee": {}})
+
+
+class FakeAleoCall:
+ def __init__(self, fake: "FakeBridge", program_id: str, function_name: str, inputs: list[str],
+ tx_id: str, make_result: Callable[[str], Any]) -> None:
+ self.fake, self.program_id, self.function_name, self.inputs = fake, program_id, function_name, inputs
+ self.tx_id, self._make = tx_id, make_result
+
+ def simulate(self, account=None):
+ self.fake.events.append(("simulate", self.function_name))
+ return "authorized"
+
+ def prove(self, account=None, **fee) -> PreparedTx:
+ self.fake.events.append(("prove", self.tx_id))
+ return PreparedTx(self.tx_id, serialized_tx(self.tx_id))
+
+ def delegate_prepared(self, account=None, **fee) -> PreparedTx:
+ self.fake.events.append(("delegate_prepared", self.tx_id))
+ return PreparedTx(self.tx_id, serialized_tx(self.tx_id))
+
+ def submit_prepared(self, prepared: PreparedTx, *, wait=True, wait_timeout=180.0):
+ self.fake.events.append(("submit", prepared.transaction_id, wait))
+ if self.fake.submit_error is not None:
+ # The bytes are already checkpointed and may be on the wire: an ambiguous broadcast.
+ raise self.fake.submit_error
+ self.fake.submitted.append(prepared.serialized)
+ return self._make(prepared.transaction_id)
+
+ def transact(self, account=None, **fee):
+ self.fake.events.append(("transact", self.tx_id))
+ return self._make(self.tx_id)
+
+ def delegate(self, account=None, **kw):
+ self.fake.events.append(("delegate", self.tx_id))
+ return self._make(self.tx_id)
+
+
+class FakeEvmCall:
+ """Mirrors ``EvmCall``/``SolCall``: every broadcast boundary — each approval and the final
+ transaction — is reduced to a ``Checkpoint``, handed to ``on_checkpoint`` and only then saved
+ to the bound store, exactly like the real calls' own channel. Without a plan (the non-plan
+ call form) there is nothing to reduce against, so the raw receipt is passed through instead.
+ """
+
+ def __init__(self, fake: "FakeBridge", intermediates: list[Receipt], final: Any, *,
+ plan: Any = None, store: Any = None, send_error: Exception | None = None) -> None:
+ self.fake, self.intermediates, self.final = fake, intermediates, final
+ self.plan, self.store = plan, store
+ self.send_error = send_error # raised after the intermediates: an ambiguous broadcast
+
+ def build(self) -> list[dict]:
+ return [{"to": "0xrouter", "data": "0x", "value": 0}]
+
+ def _emit(self, receipt: Receipt, on_checkpoint) -> None:
+ payload = receipt if self.plan is None else create_checkpoint(self.plan, receipt, DEFAULT_REGISTRY)
+ if on_checkpoint is not None:
+ on_checkpoint(payload) # the caller's channel first: the tx is already on the wire
+ if self.store is not None and isinstance(payload, Checkpoint):
+ self.store.save(payload)
+
+ def send(self, *, wait=True, timeout_seconds=120.0, poll_seconds=1.0, on_checkpoint=None):
+ self.fake.events.append(("evm_send", timeout_seconds, poll_seconds))
+ for receipt in self.intermediates:
+ self._emit(receipt, on_checkpoint)
+ if self.send_error is not None:
+ # Mirrors the real call: the transaction is armed (approvals are already checkpointed)
+ # but its own outcome is unknown — a single-use call that must never be retried.
+ raise self.send_error
+ self._emit(self.final.receipt, on_checkpoint)
+ return self.final
+
+
+FakeSolCall = FakeEvmCall
+
+
+@dataclass
+class FakeConnection:
+ address: str | None
+ can_sign: bool = True
+
+
+class FakeHyperlane:
+ def __init__(self, fake: "FakeBridge") -> None:
+ self.fake = fake
+ self.gas = GasQuote(route_id="hyperlane:aleo/eth->ethereum/eth", gas_limit=44_000,
+ gas_overhead=159_337, gas_price=1_000_000_000, exchange_rate=402,
+ payment_microcredits=8_174_147)
+ self.delivered: dict[str, bool] = {}
+
+ def quote_gas_payment(self, asset) -> GasQuote:
+ self.fake.calls.append(("hyperlane.quote_gas_payment", asset))
+ return replace(self.gas, route_id=f"hyperlane:{asset}->{OUTBOUND[asset]}")
+
+ def transfer_remote(self, asset, recipient, *, amount=None, amount_atomic=None,
+ as_signer=False, gas_payment_microcredits=None) -> FakeAleoCall:
+ kw = dict(asset=asset, recipient=recipient, amount=amount, amount_atomic=amount_atomic,
+ as_signer=as_signer, gas_payment_microcredits=gas_payment_microcredits)
+ self.fake.calls.append(("hyperlane.transfer_remote", kw))
+ route_id = f"hyperlane:{asset}->{OUTBOUND[asset]}"
+ program = f"hyp_warp_token_{asset.split('/')[1]}_v2.aleo"
+ fn = "transfer_remote_as_signer" if as_signer else "transfer_remote"
+
+ def make(tx_id: str) -> DispatchReceipt:
+ receipt = Receipt(id=tx_id, protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=tx_id,
+ protocol_state={"routeId": route_id, "sourceProgram": program,
+ "sourceFunction": fn})
+ return DispatchReceipt(tx_id, route_id, None, amount_atomic or 0, receipt)
+
+ return FakeAleoCall(self.fake, program, fn, ["<7 literals>"] * 7, self.fake.next_tx_id(), make)
+
+ def is_delivered(self, message_id) -> bool:
+ key = message_id if isinstance(message_id, str) else "0x" + bytes(message_id).hex()
+ self.fake.calls.append(("hyperlane.is_delivered", key))
+ return self.delivered.get(key, False)
+
+
+class FakeXReserve:
+ def __init__(self, fake: "FakeBridge") -> None:
+ self.fake = fake
+ self.attestations: dict[str, Attestation] = {} # messageHash hex -> Attestation
+ self.delivered_nonces: set[str] = set()
+ self.expected_secret_nonce: str | None = None # private_mint raises when it differs
+
+ def _route(self):
+ env = self.fake.environment
+ return ("xreserve:aleo/usdcx->ethereum/usdc" if env == "mainnet"
+ else "xreserve:aleo-testnet/usdcx->sepolia/usdc")
+
+ def burn(self, recipient, *, amount=None, amount_atomic=None, mode="private",
+ record=None, merkle_proof=None) -> FakeAleoCall:
+ kw = dict(recipient=recipient, amount=amount, amount_atomic=amount_atomic, mode=mode,
+ record=record, merkle_proof=merkle_proof)
+ self.fake.calls.append(("xreserve.burn", kw))
+ route_id = self._route()
+ program = "shielded_usdcx_wrapper.aleo" if mode == "private" else "usdcx_bridge_v2.aleo"
+ fn = {"private": "private_burn", "public": "burn_public",
+ "public-as-signer": "burn_public_as_signer"}[mode]
+
+ def make(tx_id: str) -> BurnReceipt:
+ receipt = Receipt(id=tx_id, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=tx_id,
+ protocol_state={"routeId": route_id, "burnMode": mode,
+ "sourceProgram": program, "sourceFunction": fn})
+ return BurnReceipt(tx_id, route_id, mode, amount_atomic or 0, receipt)
+
+ return FakeAleoCall(self.fake, program, fn, [""], self.fake.next_tx_id(), make)
+
+ def private_mint(self, attestation: Attestation, recipient, *, secret_nonce="0scalar",
+ route=None) -> FakeAleoCall:
+ self.fake.calls.append(("xreserve.private_mint", {"recipient": recipient,
+ "secret_nonce": secret_nonce,
+ "message_hash": "0x" + attestation.message_hash.hex()}))
+ if self.expected_secret_nonce is not None and secret_nonce != self.expected_secret_nonce:
+ raise AttestationError("Private mint secret nonce and recipient do not match the attested hook data")
+ route_id = route.id if route is not None else "xreserve:ethereum/usdc->aleo/usdcx"
+
+ def make(tx_id: str) -> MintReceipt:
+ receipt = Receipt(id=tx_id, protocol="xreserve", status=Status.DESTINATION_CONFIRMING,
+ destination_tx_id=tx_id, protocol_state={"routeId": route_id})
+ return MintReceipt(tx_id, route_id, receipt)
+
+ return FakeAleoCall(self.fake, "shielded_usdcx_wrapper.aleo", "private_mint",
+ ["<5 inputs>"], self.fake.next_tx_id(), make)
+
+ def get_attestation(self, message_hash, *, route=None) -> Attestation | None:
+ key = message_hash if isinstance(message_hash, str) else "0x" + bytes(message_hash).hex()
+ self.fake.calls.append(("xreserve.get_attestation", key.lower()))
+ return self.attestations.get(key.lower())
+
+ def is_delivered(self, nonce, *, route=None) -> bool:
+ key = nonce if isinstance(nonce, str) else "0x" + bytes(nonce).hex()
+ self.fake.calls.append(("xreserve.is_delivered", key.lower()))
+ return key.lower() in self.delivered_nonces
+
+
+class FakeEth:
+ """Mirrors ``aleo_bridge.eth.EthModule``'s public surface for lifecycle tests.
+
+ ``quote_transfer_remote``/``quote_deposit_usdc`` accept ``plan=`` exactly like the real
+ module: mutually exclusive with ``asset=``/``route=``/``sender=`` (a ``ValueError`` otherwise),
+ checked against ``DEFAULT_REGISTRY.version``, and the returned quote carries that same ``plan``
+ object (``.plan is plan``) — ``lifecycle.quote`` is what canonicalizes the plan on the result, so
+ the fake does not need to rebuild one the way the real module does.
+
+ The write side mirrors the same signatures: ``transfer_remote(asset=None, recipient=None, *,
+ amount=None, amount_atomic=None, plan=None)`` and ``deposit_usdc(recipient=None, *, amount=None,
+ amount_atomic=None, mint_mode=None, secret_nonce="0scalar", plan=None)``, with the real module's
+ guards — ``plan`` plus ``asset`` is a ``ValueError``, no plan and no recipient is an
+ ``InvalidRecipientError``, a stale plan is a ``RegistryVersionMismatchError``, and a plan whose
+ sender is not the connected account is a ``ConfigurationError``.
+ """
+
+ def __init__(self, fake: "FakeBridge", address: str) -> None:
+ self.fake, self.address = fake, address
+ self.approval_required = False
+ self.balances: dict[str, int] = {}
+ self.delivered: dict[str, bool] = {}
+ self.hook_data = bytes([0]) + b"\x00" * 64
+ self.source_status_result: Receipt | None = None
+ self.recover_result: Receipt | None = None
+ self.intermediates: list[Receipt] = []
+ self.send_error: Exception | None = None # raised by send() after the intermediates
+
+ def quote_transfer_remote(self, asset=None, recipient=None, *, amount=None, amount_atomic=None,
+ route=None, sender=None, plan=None):
+ if plan is not None:
+ if asset is not None or route is not None or sender is not None:
+ raise ValueError("Pass plan= or asset=/route=/sender=, not both")
+ if plan.registry_version != DEFAULT_REGISTRY.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {DEFAULT_REGISTRY.version}")
+ self.fake.calls.append(("eth.quote_transfer_remote", {"plan": plan}))
+ return EvmHyperlaneQuote(kind="evm-hyperlane", plan=plan, fees=(), amount_out=None,
+ recipient_bytes32=b"\x00" * 32,
+ native_value_atomic=plan.amount_atomic + 1000,
+ native_fee_atomic=1000, approval_required=self.approval_required)
+ self.fake.calls.append(("eth.quote_transfer_remote", dict(asset=asset, recipient=recipient,
+ amount_atomic=amount_atomic)))
+ return EvmHyperlaneQuote(kind="evm-hyperlane", plan=None, fees=(), amount_out=None,
+ recipient_bytes32=b"\x00" * 32, native_value_atomic=(amount_atomic or 0) + 1000,
+ native_fee_atomic=1000, approval_required=self.approval_required)
+
+ def _check_plan(self, plan) -> None:
+ if plan.registry_version != DEFAULT_REGISTRY.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {DEFAULT_REGISTRY.version}")
+ if plan.sender and plan.sender.lower() != self.address.lower():
+ raise ConfigurationError(f"Prepared sender {plan.sender} does not match connected account {self.address}")
+
+ def transfer_remote(self, asset=None, recipient=None, *, amount=None, amount_atomic=None,
+ plan=None) -> FakeEvmCall:
+ if plan is not None:
+ if asset is not None:
+ raise ValueError("Pass plan= or asset=, not both")
+ self._check_plan(plan)
+ self.fake.calls.append(("eth.transfer_remote", {"plan": plan}))
+ route_id, recipient, amount_atomic = plan.route_id, plan.recipient, plan.amount_atomic
+ else:
+ if recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ if asset is None:
+ raise ValueError("transfer_remote needs asset= or plan=")
+ self.fake.calls.append(("eth.transfer_remote", dict(asset=asset, recipient=recipient,
+ amount_atomic=amount_atomic)))
+ route_id = f"hyperlane:{asset}->aleo/{asset.split('/')[1]}"
+ tx = "0x" + "aa" * 32
+ receipt = Receipt(id=tx, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=tx,
+ protocol_state={"routeId": route_id, "approvalTxIds": [], "sourceSender": self.address,
+ "amountAtomic": str(amount_atomic or 0)})
+ return FakeEvmCall(self.fake, self.intermediates,
+ DispatchReceipt(tx, route_id, None, amount_atomic or 0, receipt),
+ plan=plan, store=self.fake.checkpoints, send_error=self.send_error)
+
+ def quote_deposit_usdc(self, recipient=None, *, amount=None, amount_atomic=None, mint_mode=None,
+ secret_nonce="0scalar", sender=None, route=None, plan=None):
+ if plan is not None:
+ if route is not None or sender is not None:
+ raise ValueError("Pass plan= or route=/sender=, not both")
+ if plan.registry_version != DEFAULT_REGISTRY.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {DEFAULT_REGISTRY.version}")
+ self.fake.calls.append(("eth.quote_deposit_usdc", {"plan": plan, "secret_nonce": secret_nonce}))
+ return EvmXReserveQuote(kind="evm-xreserve", plan=plan, fees=(), amount_out=None,
+ hook_data=self.hook_data, remote_recipient_bytes32=b"\x00" * 32,
+ balance_atomic=10_000_000,
+ allowance_atomic=0 if self.approval_required else 10_000_000,
+ approval_required=self.approval_required, max_fee_atomic=100_000)
+ mint_mode = "public" if mint_mode is None else mint_mode
+ self.fake.calls.append(("eth.quote_deposit_usdc", dict(recipient=recipient, amount_atomic=amount_atomic,
+ mint_mode=mint_mode, secret_nonce=secret_nonce)))
+ return EvmXReserveQuote(kind="evm-xreserve", plan=None, fees=(), amount_out=None, hook_data=self.hook_data,
+ remote_recipient_bytes32=b"\x00" * 32, balance_atomic=10_000_000,
+ allowance_atomic=0 if self.approval_required else 10_000_000,
+ approval_required=self.approval_required, max_fee_atomic=100_000)
+
+ def deposit_usdc(self, recipient=None, *, amount=None, amount_atomic=None, mint_mode=None,
+ secret_nonce="0scalar", plan=None) -> FakeEvmCall:
+ if plan is not None:
+ self._check_plan(plan)
+ self.fake.calls.append(("eth.deposit_usdc", {"plan": plan, "secret_nonce": secret_nonce}))
+ recipient, amount_atomic = plan.recipient, plan.amount_atomic
+ mint_mode = plan.mint_mode if mint_mode is None else mint_mode
+ route_id = plan.route_id
+ else:
+ if recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ mint_mode = "public" if mint_mode is None else mint_mode
+ self.fake.calls.append(("eth.deposit_usdc", dict(recipient=recipient, amount_atomic=amount_atomic,
+ mint_mode=mint_mode, secret_nonce=secret_nonce)))
+ route_id = ("xreserve:ethereum/usdc->aleo/usdcx" if self.fake.environment == "mainnet"
+ else "xreserve:sepolia/usdc->aleo-testnet/usdcx")
+ tx = "0x" + "bb" * 32
+ message_hash = "0x" + "cc" * 32
+ receipt = Receipt(id=message_hash, protocol="xreserve", status=Status.ATTESTATION_PENDING, source_tx_id=tx,
+ protocol_state={"routeId": route_id, "approvalTxIds": [], "sourceSender": self.address,
+ "mintMode": mint_mode, "intendedRecipient": recipient,
+ "hookData": "0x" + self.hook_data.hex(), "nonce": "0x" + "dd" * 32,
+ "payload": "0x" + "ee" * 305, "messageHash": message_hash,
+ "bridgeProgram": "usdcx_bridge_v2.aleo"})
+ return FakeEvmCall(self.fake, self.intermediates,
+ DepositReceipt(tx, route_id, message_hash, "0x" + "dd" * 32, receipt),
+ plan=plan, store=self.fake.checkpoints, send_error=self.send_error)
+
+ def balance(self, asset) -> int:
+ self.fake.calls.append(("eth.balance", asset))
+ return self.balances.get(asset, 0)
+
+ def is_delivered(self, message_id) -> bool:
+ key = message_id if isinstance(message_id, str) else "0x" + bytes(message_id).hex()
+ self.fake.calls.append(("eth.is_delivered", key))
+ return self.delivered.get(key, False)
+
+ def source_status(self, plan, receipt) -> Receipt:
+ self.fake.calls.append(("eth.source_status", receipt.status))
+ return self.source_status_result or receipt
+
+ def recover_source(self, plan, checkpoint, *, required=False) -> Receipt:
+ self.fake.calls.append(("eth.recover_source", checkpoint.to_dict(), required))
+ assert self.recover_result is not None, "script FakeEth.recover_result first"
+ return self.recover_result
+
+
+class FakeSol:
+ """Mirrors ``aleo_bridge.sol.SolModule``'s public surface for lifecycle tests.
+
+ ``quote_transfer_remote(recipient=None, *, amount=None, amount_atomic=None, sender=None,
+ plan=None)`` and ``transfer_remote(recipient=None, *, amount=None, amount_atomic=None,
+ plan=None)`` mirror the real ``SolModule`` signatures: ``plan=`` alone is enough (it supplies
+ recipient, amount and sender), ``plan`` together with ``sender=`` is a ``ValueError``, and
+ neither a plan nor a recipient is an ``InvalidRecipientError``. There is no ``asset=``/``route=``
+ kwarg to conflict with, so a plan overrides recipient/amount silently — the real module's
+ behavior, not the Eth-like ``ValueError`` on every conflict.
+
+ Solana addresses are base58 and therefore case-SENSITIVE: the plan-sender check compares them
+ exactly, unlike ``FakeEth``'s case-insensitive EVM comparison.
+ """
+
+ def __init__(self, fake: "FakeBridge", address: str) -> None:
+ self.fake, self.address = fake, address
+ self.balance_lamports = 0
+ self.source_status_result: Receipt | None = None
+ self.intermediates: list[Receipt] = []
+ self.transaction_logs_result: list[str] | None = None
+ self.transaction_logs_error: Exception | None = None
+
+ def _check_plan(self, plan) -> None:
+ if plan.registry_version != DEFAULT_REGISTRY.version:
+ raise RegistryVersionMismatchError(
+ f"Plan uses registry {plan.registry_version}; this client has {DEFAULT_REGISTRY.version}")
+ if plan.sender and plan.sender != self.address: # base58: compared exactly
+ raise ConfigurationError(f"Prepared sender {plan.sender} does not match connected account {self.address}")
+
+ def quote_transfer_remote(self, recipient=None, *, amount=None, amount_atomic=None, sender=None, plan=None):
+ if plan is not None:
+ if sender is not None:
+ raise ValueError("Pass plan= or sender=, not both: the plan carries its own sender")
+ self._check_plan(plan)
+ self.fake.calls.append(("sol.quote_transfer_remote", {"plan": plan}))
+ return SolanaHyperlaneQuote(kind="solana-hyperlane", plan=plan, fees=(), amount_out=None,
+ igp_lamports=2_900_000, network_fee_lamports=10_000, rent_lamports=5_004_240,
+ total_lamports=plan.amount_atomic + 7_914_240,
+ unique_message_address="uniq1111111111111111111111111111111111111111")
+ if recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ self.fake.calls.append(("sol.quote_transfer_remote", dict(recipient=recipient, amount_atomic=amount_atomic)))
+ return SolanaHyperlaneQuote(kind="solana-hyperlane", plan=None, fees=(), amount_out=None,
+ igp_lamports=2_900_000, network_fee_lamports=10_000, rent_lamports=5_004_240,
+ total_lamports=(amount_atomic or 0) + 7_914_240,
+ unique_message_address="uniq1111111111111111111111111111111111111111")
+
+ def transfer_remote(self, recipient=None, *, amount=None, amount_atomic=None, plan=None) -> FakeSolCall:
+ if plan is not None:
+ self._check_plan(plan)
+ self.fake.calls.append(("sol.transfer_remote", {"plan": plan}))
+ recipient, amount_atomic = plan.recipient, plan.amount_atomic
+ else:
+ if recipient is None:
+ raise InvalidRecipientError("recipient is required when no plan is given")
+ self.fake.calls.append(("sol.transfer_remote", dict(recipient=recipient, amount_atomic=amount_atomic)))
+ route_id = "hyperlane:solana/sol->aleo/sol"
+ sig = "5igNature" * 8
+ receipt = Receipt(id=sig, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=sig,
+ protocol_state={"routeId": route_id, "signature": sig, "blockhash": "recent",
+ "lastValidBlockHeight": "123456789"})
+ return FakeSolCall(self.fake, self.intermediates,
+ DispatchReceipt(sig, route_id, None, amount_atomic or 0, receipt),
+ plan=plan, store=self.fake.checkpoints)
+
+ def balance(self) -> int:
+ self.fake.calls.append(("sol.balance",))
+ return self.balance_lamports
+
+ def source_status(self, plan, receipt) -> Receipt:
+ self.fake.calls.append(("sol.source_status", receipt.status))
+ return self.source_status_result or receipt
+
+ def _transaction_logs(self, signature) -> list[str] | None:
+ self.fake.calls.append(("sol._transaction_logs", signature))
+ if self.transaction_logs_error is not None:
+ raise self.transaction_logs_error
+ return self.transaction_logs_result
+
+
+class FakeBridge:
+ """Duck-typed stand-in for ``aleo_bridge.client.Bridge`` (no network, no extras)."""
+
+ def __init__(self, *, environment="mainnet", ethereum=True, solana=False, checkpoints=None) -> None:
+ self.registry = DEFAULT_REGISTRY
+ self.environment = self.network = environment
+ self.checkpoints = checkpoints
+ self.events: list[tuple] = [] # ordered side effects (prove/submit/checkpoint...)
+ self.calls: list[tuple] = [] # module method calls with kwargs
+ self.submitted: list[str] = []
+ self.submit_error: Exception | None = None # raised by submit_prepared, after the checkpoint
+ self._tx = 0
+ self.aleo = _ConftestFakeAleo(mappings=default_mappings(), network_name=environment)
+ self.hyperlane = FakeHyperlane(self)
+ self.xreserve = FakeXReserve(self)
+ self.ethereum = FakeConnection(EVM_ADDRESS) if ethereum else None
+ self._eth = FakeEth(self, EVM_ADDRESS) if ethereum else None
+ self.solana = FakeConnection(SOL_ADDRESS) if solana else None
+ self._sol = FakeSol(self, SOL_ADDRESS) if solana else None
+ self.public_balances: dict[str, int] = {}
+
+ @property
+ def eth(self) -> FakeEth:
+ """Mirrors the real ``Bridge.eth`` property: ``ConfigurationError`` when ``ethereum`` is None."""
+ if self._eth is None:
+ raise ConfigurationError("Pass ethereum=Ethereum(...) to Bridge(...) or set ETHEREUM_RPC_URL")
+ return self._eth
+
+ @property
+ def sol(self) -> FakeSol:
+ """Mirrors the real ``Bridge.sol`` property: ``ConfigurationError`` when ``solana`` is None."""
+ if self._sol is None:
+ raise ConfigurationError(
+ "Solana is not configured: pass solana=Solana(rpc_url, private_key=...) or a solana-py Client to Bridge(), "
+ "or set SOLANA_PRIVATE_KEY (and optionally SOLANA_RPC_URL) for Bridge.from_env()")
+ return self._sol
+
+ def next_tx_id(self) -> str:
+ self._tx += 1
+ return f"at1fake{self._tx}"
+
+ def aleo_address(self) -> str:
+ return ALEO_RECIPIENT
+
+ # Plan-1 surface the agent tools touch
+ def shield(self, asset, *, amount=None, amount_atomic=None, recipient=None) -> FakeAleoCall:
+ self.calls.append(("shield", dict(asset=asset, amount=amount, amount_atomic=amount_atomic)))
+ return FakeAleoCall(self, "arc20_eth.aleo", "shield", [f"{amount_atomic}u128"], self.next_tx_id(),
+ lambda tx: PrivacyReceipt(tx, asset, str(amount or amount_atomic), amount_atomic or 0, "shield"))
+
+ def unshield(self, asset, *, amount=None, amount_atomic=None, record=None, merkle_proof=None, recipient=None):
+ self.calls.append(("unshield", dict(asset=asset, amount=amount, amount_atomic=amount_atomic)))
+ return FakeAleoCall(self, "arc20_eth.aleo", "unshield", [RECORD_PLAINTEXT, f"{amount_atomic}u128"], self.next_tx_id(),
+ lambda tx: PrivacyReceipt(tx, asset, str(amount or amount_atomic), amount_atomic or 0, "unshield"))
+
+ def status(self) -> BridgeStatus:
+ chains = [ChainStatus("aleo" if self.environment == "mainnet" else "aleo-testnet", ALEO_RECIPIENT, True,
+ dict(self.public_balances))]
+ if self.ethereum is not None:
+ chains.append(ChainStatus("ethereum", self.ethereum.address, True, dict(self.eth.balances)))
+ if self.solana is not None:
+ chains.append(ChainStatus("solana", self.solana.address, True, {"solana/sol": self.sol.balance_lamports}))
+ from aleo_bridge.lifecycle import recover
+ pending = [recover(self, cp) for cp in self.checkpoints.list()] if self.checkpoints else []
+ return BridgeStatus(self.environment, self.registry.version, chains, pending)
diff --git a/bridge-sdk/tests/fakes/fake_solana.py b/bridge-sdk/tests/fakes/fake_solana.py
new file mode 100644
index 0000000..74c2799
--- /dev/null
+++ b/bridge-sdk/tests/fakes/fake_solana.py
@@ -0,0 +1,151 @@
+"""Fake solana-py ``Client`` for SolModule tests.
+
+Mirrors solana-py 0.40's shapes: every method returns an object with ``.value``;
+``get_latest_blockhash().value`` has ``blockhash``/``last_valid_block_height``;
+``get_account_info().value`` is ``None`` or has ``.data: bytes``; signature statuses are a
+list with ``err``/``confirmation_status``; ``get_transaction().value.transaction.meta.log_messages``.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from types import SimpleNamespace
+from typing import Any
+
+from solders.hash import Hash
+from solders.signature import Signature
+from solders.transaction import VersionedTransaction
+
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from tests.fakes.sealevel_fixtures import (
+ DISPATCHED_MESSAGE_RENT_LAMPORTS,
+ FEE_PAYER_RENT_LAMPORTS,
+ GAS_PAYMENT_RENT_LAMPORTS,
+ IGP,
+ NETWORK_FEE_LAMPORTS,
+ TRANSFER,
+ WARP_PROGRAM_ADDRESS,
+ igp_account_data,
+)
+
+# Any 32-byte base58 string is a valid blockhash for compile/sign purposes (veil uses the warp program id).
+BLOCKHASH = Hash.from_string(WARP_PROGRAM_ADDRESS)
+LAST_VALID_BLOCK_HEIGHT = 100
+RENTS = {141: GAS_PAYMENT_RENT_LAMPORTS, 194: DISPATCHED_MESSAGE_RENT_LAMPORTS, 0: FEE_PAYER_RENT_LAMPORTS}
+STUB_SIGNATURE = Signature.from_bytes(bytes([7]) * 64)
+
+
+class _Resp:
+ def __init__(self, value: Any) -> None:
+ self.value = value
+
+
+@dataclass
+class FakeSignatureStatus:
+ err: Any = None
+ confirmation_status: str | None = "confirmed"
+
+
+@dataclass
+class _Blockhash:
+ blockhash: Hash
+ last_valid_block_height: int
+
+
+@dataclass
+class _Account:
+ data: bytes
+
+
+class FakeSolanaClient:
+ """``statuses`` is consumed one entry per ``get_signature_statuses`` call (the last entry repeats);
+ an entry may be ``None`` (unknown signature), a ``FakeSignatureStatus``, or an ``Exception`` to raise."""
+
+ def __init__(self, *, balance: int = 800_000_000_000, accounts: dict[str, bytes] | None = None,
+ fee: int = NETWORK_FEE_LAMPORTS, rents: dict[int, int] | None = None,
+ statuses: list[Any] | None = None, blockhash_valid: Any = True,
+ logs: list[str] | None = None, no_logs: bool = False,
+ signature: Signature | None = None, get_transaction_error: Exception | None = None,
+ send_error: Exception | None = None) -> None:
+ self.balance = balance
+ self.accounts = {IGP["address"]: igp_account_data()} if accounts is None else accounts
+ self.fee = fee
+ self.rents = RENTS if rents is None else rents
+ self.statuses = list(statuses) if statuses is not None else [FakeSignatureStatus()]
+ self.blockhash_valid = blockhash_valid
+ # logs=None → the recorded mainnet logs; logs=[] → confirmed but no dispatch line; no_logs → transaction not found
+ self.logs = None if no_logs else (list(TRANSFER["logMessages"]) if logs is None else list(logs))
+ # None → echo the transaction's own fee-payer signature, as a real node does. A Signature here
+ # makes the node answer with a DIFFERENT id than the one the client signed (the mismatch case).
+ self.signature = signature
+ self.get_transaction_error = get_transaction_error # raised by get_transaction (RPC/decode failure)
+ self.send_error = send_error # raised by send_raw_transaction (response lost)
+ self.calls: list[str] = []
+ self.fee_messages: list[Any] = []
+ self.sent: list[bytes] = []
+ self.sent_opts: list[Any] = []
+ self.status_calls: list[bool] = []
+ self.transaction_calls: list[tuple[Any, Any]] = []
+
+ def get_latest_blockhash(self, commitment=None):
+ self.calls.append("get_latest_blockhash")
+ return _Resp(_Blockhash(BLOCKHASH, LAST_VALID_BLOCK_HEIGHT))
+
+ def get_balance(self, pubkey, commitment=None):
+ self.calls.append("get_balance")
+ return _Resp(self.balance)
+
+ def get_account_info(self, pubkey, commitment=None, encoding="base64", data_slice=None):
+ self.calls.append("get_account_info")
+ data = self.accounts.get(str(pubkey))
+ return _Resp(None if data is None else _Account(data))
+
+ def get_fee_for_message(self, message, commitment=None):
+ self.calls.append("get_fee_for_message")
+ self.fee_messages.append(message)
+ return _Resp(self.fee)
+
+ def get_minimum_balance_for_rent_exemption(self, usize, commitment=None):
+ self.calls.append("get_minimum_balance_for_rent_exemption")
+ return _Resp(self.rents[usize])
+
+ def send_raw_transaction(self, txn, opts=None):
+ self.calls.append("send_raw_transaction")
+ raw = bytes(txn)
+ self.sent.append(raw)
+ self.sent_opts.append(opts)
+ if self.send_error is not None:
+ raise self.send_error
+ return _Resp(self.signature if self.signature is not None else VersionedTransaction.from_bytes(raw).signatures[0])
+
+ def sent_signature(self, index: int = 0) -> str:
+ """Fee-payer signature of the index-th broadcast transaction — the id the node echoed back."""
+ return str(VersionedTransaction.from_bytes(self.sent[index]).signatures[0])
+
+ def get_signature_statuses(self, signatures, search_transaction_history=False):
+ self.calls.append("get_signature_statuses")
+ self.status_calls.append(search_transaction_history)
+ item = self.statuses.pop(0) if len(self.statuses) > 1 else self.statuses[0]
+ if isinstance(item, Exception):
+ raise item
+ return _Resp([item])
+
+ def is_blockhash_valid(self, blockhash, commitment=None):
+ self.calls.append("is_blockhash_valid")
+ if isinstance(self.blockhash_valid, Exception):
+ raise self.blockhash_valid
+ return _Resp(self.blockhash_valid)
+
+ def get_transaction(self, tx_sig, encoding="json", commitment=None, max_supported_transaction_version=None):
+ self.calls.append("get_transaction")
+ self.transaction_calls.append((commitment, max_supported_transaction_version))
+ if self.get_transaction_error is not None:
+ raise self.get_transaction_error
+ if self.logs is None:
+ return _Resp(None)
+ meta = SimpleNamespace(log_messages=list(self.logs))
+ return _Resp(SimpleNamespace(transaction=SimpleNamespace(meta=meta)))
+
+
+def stub_bridge(environment: str = "mainnet", checkpoints: Any = None) -> SimpleNamespace:
+ """What SolModule needs from a Bridge without constructing one: registry, environment, checkpoint store."""
+ return SimpleNamespace(registry=DEFAULT_REGISTRY, environment=environment, checkpoints=checkpoints)
diff --git a/bridge-sdk/tests/fakes/fake_web3.py b/bridge-sdk/tests/fakes/fake_web3.py
new file mode 100644
index 0000000..5e93b5a
--- /dev/null
+++ b/bridge-sdk/tests/fakes/fake_web3.py
@@ -0,0 +1,332 @@
+"""A hand-rolled JSON-RPC provider behind a real ``web3.Web3``.
+
+The real web3 contract/ABI/event/signing stack runs unchanged; only the
+transport is fake, so tests exercise the exact calldata, event decoding and
+raw-transaction signing that production uses. State is plain dicts the test
+mutates directly.
+
+The Aleo-side fake facade lives in ``tests/conftest.py`` (``FakeAleo``,
+``bridge``/``fake_aleo`` fixtures) — this module holds only web3-specific
+fakes so there is exactly one Aleo fake in the suite.
+"""
+from __future__ import annotations
+
+from typing import Any, Callable
+
+from eth_abi import decode, encode
+from eth_account import Account
+from eth_utils import keccak, to_checksum_address
+from hexbytes import HexBytes
+from web3 import Web3
+from web3.providers import BaseProvider
+
+ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
+BLOCK_HASH = "0x" + "cd" * 32
+SELECTORS = {
+ keccak(text="balanceOf(address)")[:4]: "balanceOf",
+ keccak(text="allowance(address,address)")[:4]: "allowance",
+ keccak(text="quoteTransferRemote(uint32,bytes32,uint256)")[:4]: "quoteTransferRemote",
+ keccak(text="delivered(bytes32)")[:4]: "delivered",
+}
+TOPIC_DISPATCH_ID = "0x" + keccak(text="DispatchId(bytes32)").hex()
+TOPIC_SENT_TRANSFER_REMOTE = "0x" + keccak(text="SentTransferRemote(uint32,bytes32,uint256)").hex()
+TOPIC_DEPOSITED_TO_REMOTE = "0x" + keccak(
+ text="DepositedToRemote(address,uint256,address,bytes32,uint32,bytes32,uint256,bytes)"
+).hex()
+
+
+def tx_hash_for(n: int) -> str:
+ """Deterministic hash of the n-th (1-based) transaction the fake accepted through ``eth_sendTransaction``.
+
+ Raw (locally signed) transactions get their REAL hash — ``keccak(raw)`` — because eth.py now
+ checks the node's echoed hash against the one it computed locally, exactly as a real node would
+ answer. Use ``provider.hash_at(n)`` to read a hash back after the send, and the ``*_nth`` knobs
+ to arm ``pending``/``reverted``/``receipt_delay`` for a transaction not yet broadcast.
+ """
+ return "0x" + keccak(text=f"fake-tx-{n}").hex()
+
+
+def _hex(n: int) -> str:
+ return hex(n)
+
+
+def event_log(address: str, topics: list[str], data: str, *, log_index: int, tx_hash: str,
+ block_number: int = 0x11) -> dict:
+ return {"address": to_checksum_address(address), "topics": topics, "data": data,
+ "logIndex": _hex(log_index), "blockNumber": _hex(block_number), "transactionHash": tx_hash,
+ "transactionIndex": "0x0", "blockHash": BLOCK_HASH, "removed": False}
+
+
+def dispatch_id_log(mailbox: str, message_id: bytes, *, tx_hash: str, log_index: int = 5, block_number: int = 0x11) -> dict:
+ return event_log(mailbox, [TOPIC_DISPATCH_ID, "0x" + message_id.hex()], "0x",
+ log_index=log_index, tx_hash=tx_hash, block_number=block_number)
+
+
+def sent_transfer_remote_log(router: str, *, destination: int, recipient32: bytes, amount: int, tx_hash: str,
+ log_index: int = 4, block_number: int = 0x65) -> dict:
+ topics = [TOPIC_SENT_TRANSFER_REMOTE, "0x" + encode(["uint32"], [destination]).hex(), "0x" + recipient32.hex()]
+ return event_log(router, topics, "0x" + encode(["uint256"], [amount]).hex(),
+ log_index=log_index, tx_hash=tx_hash, block_number=block_number)
+
+
+def deposited_log(xreserve: str, *, local_token: str, depositor: str, remote_recipient32: bytes, value: int,
+ remote_domain: int, remote_token32: bytes, max_fee: int, hook_data: bytes, tx_hash: str,
+ log_index: int = 3, block_number: int = 0x65) -> dict:
+ topics = [TOPIC_DEPOSITED_TO_REMOTE, "0x" + encode(["address"], [local_token]).hex(),
+ "0x" + encode(["address"], [depositor]).hex(), "0x" + remote_recipient32.hex()]
+ data = encode(["uint256", "uint32", "bytes32", "uint256", "bytes"],
+ [value, remote_domain, remote_token32, max_fee, hook_data])
+ return event_log(xreserve, topics, "0x" + data.hex(), log_index=log_index, tx_hash=tx_hash, block_number=block_number)
+
+
+def _decode_raw(raw: bytes) -> dict:
+ """Signed raw tx → {to, value, data, from, nonce, gas, ...fee fields}. Typed (0x02) and legacy envelopes.
+
+ Keeps whichever fee fields the sender actually filled in (``gasPrice`` for a
+ legacy/type-0 envelope, ``maxFeePerGas``/``maxPriorityFeePerGas`` for a type-2
+ one) so tests can assert on the exact values ``eth.py``'s fee-filling logic
+ computed, not just that *some* transaction was sent.
+ """
+ from eth_account.typed_transactions import TypedTransaction
+
+ sender = Account.recover_transaction(raw)
+ if raw[0] <= 0x7F:
+ fields = TypedTransaction.from_bytes(HexBytes(raw)).as_dict()
+ else:
+ import rlp
+ from eth_account._utils.legacy_transactions import Transaction
+
+ fields = rlp.decode(raw, Transaction).as_dict()
+ data = fields.get("data", b"")
+ data_hex = data if isinstance(data, str) else "0x" + bytes(data).hex()
+ tx = {"to": to_checksum_address(fields["to"]), "value": int(fields.get("value", 0)), "data": data_hex, "from": sender}
+ for key in ("nonce", "gas", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas"):
+ if fields.get(key) is not None:
+ tx[key] = int(fields[key])
+ return tx
+
+
+class FakeRpcProvider(BaseProvider):
+ """State: balances, allowances, router quotes, delivered ids, sent txs, receipts, history logs."""
+
+ def __init__(self, *, chain_id: int = 1, eth_balances: dict[str, int] | None = None,
+ token_balances: dict[tuple[str, str], int] | None = None,
+ allowances: dict[tuple[str, str, str], int] | None = None,
+ quotes: dict[str, list[tuple[str, int]]] | None = None,
+ delivered: set[str] | None = None, legacy: bool = False) -> None:
+ super().__init__()
+ self.chain_id = chain_id
+ self.legacy = legacy # True: eth_getBlockByNumber omits baseFeePerGas
+ self.eth_balances = {to_checksum_address(k): v for k, v in (eth_balances or {}).items()}
+ self.token_balances = {(to_checksum_address(t), to_checksum_address(o)): v
+ for (t, o), v in (token_balances or {}).items()}
+ self.allowances = {(to_checksum_address(t), to_checksum_address(o), to_checksum_address(s)): v
+ for (t, o, s), v in (allowances or {}).items()}
+ self.quotes = {to_checksum_address(r): q for r, q in (quotes or {}).items()}
+ self.delivered = {d.lower() for d in (delivered or set())}
+ self.sent: list[dict] = [] # {to, value, data, from, hash} in send order
+ self.pending: set[str] = set() # hashes whose receipt stays None
+ self.reverted: set[str] = set() # hashes whose receipt has status 0
+ self.receipt_delay: dict[str, int] = {} # hash -> remaining polls that return None before mined
+ # A locally signed transaction's hash is only known once it is signed, so these arm the three
+ # knobs above by 1-based send order instead; _accept translates them the moment it accepts.
+ self.pending_nth: set[int] = set()
+ self.reverted_nth: set[int] = set()
+ self.receipt_delay_nth: dict[int, int] = {}
+ self.send_errors: dict[int, str] = {} # 1-based send -> JSON-RPC error (the response is lost)
+ self.echo_hashes: dict[int, str] = {} # 1-based send -> hash to echo INSTEAD of the real one
+ self.receipt_poll_counts: dict[str, int] = {} # hash -> eth_getTransactionReceipt calls seen for it
+ self.receipt_logs: Callable[[dict], list[dict]] = lambda tx: [] # logs for a sent tx's receipt
+ self.history_logs: list[dict] = [] # served by eth_getLogs (filtered by address/from/toBlock)
+ self.log_filters: list[dict] = [] # raw eth_getLogs filter params, in call order
+ self.log_scan_errors: dict[int, str] = {} # 1-based eth_getLogs call -> JSON-RPC error message
+ self.transactions: dict[str, dict] = {} # extra eth_getTransactionByHash answers
+ self.receipts: dict[str, dict] = {} # extra eth_getTransactionReceipt answers
+ self.block_number = 0x10
+ self.methods: list[str] = []
+
+ def _ok(self, result: Any) -> dict:
+ return {"jsonrpc": "2.0", "id": 1, "result": result}
+
+ def _block_tag(self, value: Any, default: int) -> int:
+ """A JSON-RPC block tag as an int; ``None``/``"latest"`` fall back to *default*."""
+ if value is None or value in ("latest", "pending", "safe", "finalized"):
+ return default
+ return int(value, 16) if isinstance(value, str) else int(value)
+
+ def _observe_block(self, block_number: int) -> None:
+ """Keep the head at least as high as any block the test has placed a receipt or tx in, so a
+ bounded (``fromBlock``..``eth_blockNumber``) log scan can actually reach that history."""
+ self.block_number = max(self.block_number, block_number)
+
+ def add_receipt(self, tx_hash: str, *, status: int = 1, logs: list[dict] | None = None, block_number: int = 0x65,
+ sender: str = ZERO_ADDRESS, to: str = ZERO_ADDRESS) -> None:
+ """Serve a receipt for a hash the fake never accepted itself (recovery / status tests)."""
+ self._observe_block(block_number)
+ self.receipts[tx_hash] = {
+ "transactionHash": tx_hash, "status": _hex(status), "blockNumber": _hex(block_number), "blockHash": BLOCK_HASH,
+ "transactionIndex": "0x0", "from": to_checksum_address(sender), "to": to_checksum_address(to),
+ "cumulativeGasUsed": "0x1", "gasUsed": "0x1", "effectiveGasPrice": "0x1", "type": "0x2",
+ "contractAddress": None, "logsBloom": "0x" + "00" * 256, "logs": logs or []}
+
+ def add_transaction(self, tx_hash: str, *, sender: str, to: str, block_number: int = 0x65) -> None:
+ """Serve eth_getTransactionByHash for a hash the fake never accepted itself."""
+ self._observe_block(block_number)
+ self.transactions[tx_hash] = {
+ "hash": tx_hash, "from": to_checksum_address(sender), "to": to_checksum_address(to), "input": "0x", "value": "0x0",
+ "blockNumber": _hex(block_number), "blockHash": BLOCK_HASH, "nonce": "0x0", "gas": "0x1", "gasPrice": "0x1",
+ "transactionIndex": "0x0", "type": "0x2", "chainId": _hex(self.chain_id), "v": "0x0", "r": "0x0", "s": "0x0"}
+
+ def make_request(self, method: str, params: Any) -> dict:
+ self.methods.append(method)
+ if method == "eth_chainId":
+ return self._ok(_hex(self.chain_id))
+ if method == "eth_blockNumber":
+ return self._ok(_hex(self.block_number))
+ if method == "eth_gasPrice":
+ return self._ok(_hex(10**9))
+ if method == "eth_maxPriorityFeePerGas":
+ return self._ok(_hex(10**8))
+ if method == "eth_feeHistory":
+ return self._ok({"baseFeePerGas": [_hex(10**9)] * 2, "gasUsedRatio": [0.5],
+ "oldestBlock": "0x1", "reward": [[_hex(10**8)]]})
+ if method == "eth_getBlockByNumber":
+ block = {"number": _hex(self.block_number), "gasLimit": _hex(30_000_000),
+ "gasUsed": "0x0", "timestamp": "0x0", "hash": "0x" + "ab" * 32, "parentHash": "0x" + "00" * 32,
+ "transactions": [], "difficulty": "0x0", "extraData": "0x", "logsBloom": "0x" + "00" * 256,
+ "miner": ZERO_ADDRESS, "mixHash": "0x" + "00" * 32, "nonce": "0x0000000000000000",
+ "receiptsRoot": "0x" + "00" * 32, "sha3Uncles": "0x" + "00" * 32, "size": "0x1",
+ "stateRoot": "0x" + "00" * 32, "totalDifficulty": "0x0",
+ "transactionsRoot": "0x" + "00" * 32, "uncles": []}
+ if not self.legacy:
+ block["baseFeePerGas"] = _hex(10**9)
+ return self._ok(block)
+ if method == "eth_getTransactionCount":
+ return self._ok(_hex(len(self.sent)))
+ if method == "eth_estimateGas":
+ return self._ok(_hex(150_000))
+ if method == "eth_getBalance":
+ return self._ok(_hex(self.eth_balances.get(to_checksum_address(params[0]), 0)))
+ if method == "eth_call":
+ return self._ok(self._call(params[0]))
+ if method == "eth_sendRawTransaction":
+ raw = bytes.fromhex(params[0][2:])
+ message = self.send_errors.get(len(self.sent) + 1)
+ if message is not None: # the node took it, the answer never came back
+ return {"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": message}}
+ return self._ok(self._accept(_decode_raw(raw), raw=raw))
+ if method == "eth_sendTransaction":
+ p = params[0]
+ raw_value = p.get("value", 0)
+ value = int(raw_value, 16) if isinstance(raw_value, str) else int(raw_value)
+ return self._ok(self._accept({"to": to_checksum_address(p["to"]), "value": value,
+ "data": p.get("data", "0x"), "from": to_checksum_address(p["from"])}))
+ if method == "eth_getTransactionReceipt":
+ h = params[0]
+ self.receipt_poll_counts[h] = self.receipt_poll_counts.get(h, 0) + 1
+ return self._ok(self._receipt(h))
+ if method == "eth_getLogs":
+ f = params[0]
+ self.log_filters.append(f)
+ message = self.log_scan_errors.get(len(self.log_filters))
+ if message is not None:
+ return {"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": message}}
+ addr = f.get("address")
+ addrs = {to_checksum_address(a) for a in (addr if isinstance(addr, list) else [addr])} if addr else None
+ from_block = self._block_tag(f.get("fromBlock"), 0)
+ to_block = self._block_tag(f.get("toBlock"), self.block_number)
+ return self._ok([log for log in self.history_logs
+ if (addrs is None or log["address"] in addrs)
+ and from_block <= int(log["blockNumber"], 16) <= to_block])
+ if method == "eth_getTransactionByHash":
+ h = params[0]
+ if h in self.transactions:
+ return self._ok(self.transactions[h])
+ sent = next((t for t in self.sent if t["hash"] == h), None)
+ if sent is None:
+ return self._ok(None)
+ return self._ok({"hash": h, "from": sent["from"], "to": sent["to"], "input": sent["data"],
+ "value": _hex(sent["value"]), "blockNumber": _hex(self.block_number + 1),
+ "blockHash": BLOCK_HASH, "nonce": "0x0", "gas": "0x1", "gasPrice": "0x1",
+ "transactionIndex": "0x0", "type": "0x2", "chainId": _hex(self.chain_id),
+ "v": "0x0", "r": "0x0", "s": "0x0"})
+ raise NotImplementedError(method)
+
+ def _accept(self, tx: dict, raw: bytes | None = None) -> str:
+ self.sent.append(tx)
+ n = len(self.sent)
+ tx["hash"] = ("0x" + keccak(raw).hex()) if raw is not None else tx_hash_for(n)
+ if n in self.pending_nth:
+ self.pending.add(tx["hash"])
+ if n in self.reverted_nth:
+ self.reverted.add(tx["hash"])
+ if n in self.receipt_delay_nth:
+ self.receipt_delay[tx["hash"]] = self.receipt_delay_nth[n]
+ return self.echo_hashes.get(n, tx["hash"])
+
+ def hash_at(self, n: int) -> str:
+ """Hash of the n-th (1-based) accepted transaction — the real one for a locally signed send."""
+ return self.sent[n - 1]["hash"]
+
+ def _receipt(self, h: str) -> dict | None:
+ if h in self.pending:
+ return None
+ delay = self.receipt_delay.get(h, 0)
+ if delay > 0:
+ self.receipt_delay[h] = delay - 1
+ return None
+ if h in self.receipts:
+ return self.receipts[h]
+ sent = next((t for t in self.sent if t["hash"] == h), None)
+ if sent is None:
+ return None
+ return {"transactionHash": h, "status": "0x0" if h in self.reverted else "0x1",
+ "blockNumber": _hex(self.block_number + 1), "blockHash": BLOCK_HASH, "transactionIndex": "0x0",
+ "from": sent["from"], "to": sent["to"], "cumulativeGasUsed": "0x1", "gasUsed": "0x1",
+ "effectiveGasPrice": "0x1", "type": "0x2", "contractAddress": None,
+ "logsBloom": "0x" + "00" * 256, "logs": self.receipt_logs(sent)}
+
+ def _call(self, call: dict) -> str:
+ to = to_checksum_address(call["to"])
+ data = bytes.fromhex(call["data"][2:])
+ name = SELECTORS.get(data[:4])
+ args = data[4:]
+ if name == "balanceOf":
+ (owner,) = decode(["address"], args)
+ return "0x" + encode(["uint256"], [self.token_balances.get((to, to_checksum_address(owner)), 0)]).hex()
+ if name == "allowance":
+ owner, spender = decode(["address", "address"], args)
+ key = (to, to_checksum_address(owner), to_checksum_address(spender))
+ return "0x" + encode(["uint256"], [self.allowances.get(key, 0)]).hex()
+ if name == "quoteTransferRemote":
+ return "0x" + encode(["(address,uint256)[]"], [self.quotes.get(to, [])]).hex()
+ if name == "delivered":
+ (message_id,) = decode(["bytes32"], args)
+ return "0x" + encode(["bool"], [("0x" + message_id.hex()) in self.delivered]).hex()
+ raise NotImplementedError(f"eth_call selector {data[:4].hex()} to {to}")
+
+
+def fake_web3(**config: Any) -> Web3:
+ """A real ``Web3`` over ``FakeRpcProvider``; reach the state through ``w3.provider``."""
+ return Web3(FakeRpcProvider(**config))
+
+
+def make_bridge(*, ethereum: Any = None, environment: str | None = None, checkpoints: Any = None,
+ **aleo_kwargs: Any) -> Any:
+ """A ``Bridge`` over a fresh ``FakeAleo`` (mainnet unless *environment* says otherwise), wired
+ with *ethereum* so ``bridge.eth`` works.
+
+ ``checkpoints`` forwards to ``Bridge(..., checkpoints=...)`` (a ``CheckpointStore``, e.g.
+ ``FileCheckpointStore``); it is never a ``FakeAleo`` constructor argument.
+
+ Kept here (rather than in ``tests/conftest.py``) so ``eth.py`` tests can import one fixture
+ factory alongside ``fake_web3`` without pulling in pytest fixtures.
+ """
+ from aleo_bridge import Bridge
+
+ from tests.conftest import FakeAleo, default_mappings
+
+ aleo_kwargs.setdefault("mappings", default_mappings())
+ if environment is not None:
+ aleo_kwargs.setdefault("network_name", environment)
+ return Bridge(FakeAleo(**aleo_kwargs), ethereum=ethereum, environment=environment, checkpoints=checkpoints)
diff --git a/bridge-sdk/tests/fakes/sealevel_fixtures.py b/bridge-sdk/tests/fakes/sealevel_fixtures.py
new file mode 100644
index 0000000..3d02008
--- /dev/null
+++ b/bridge-sdk/tests/fakes/sealevel_fixtures.py
@@ -0,0 +1,52 @@
+"""Golden fixtures from veil's test/fixtures (mainnet TransferRemote + inner IGP account).
+
+Pure: no solders import, so the _sealevel layout tests run on an Aleo-only install.
+"""
+from __future__ import annotations
+
+import base64
+import json
+from pathlib import Path
+
+FIXTURES = Path(__file__).resolve().parents[1] / "fixtures"
+TRANSFER: dict = json.loads((FIXTURES / "sealevel-transfer-remote.json").read_text())
+IGP: dict = json.loads((FIXTURES / "sealevel-igp-account.json").read_text())
+
+WARP_PROGRAM_ADDRESS = "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7"
+ALEO_MAINNET_DOMAIN = 1634493807
+DESTINATION_GAS_AMOUNT = 464_000
+EXPECTED_IGP_PAYMENT_LAMPORTS = 2_900_000
+NETWORK_FEE_LAMPORTS = 10_000
+GAS_PAYMENT_RENT_LAMPORTS = 1_872_240
+DISPATCHED_MESSAGE_RENT_LAMPORTS = 2_241_120
+FEE_PAYER_RENT_LAMPORTS = 890_880
+EXPECTED_MESSAGE_ID = "0xffe0409d00c184769b4dfa2a1eaac5a0a79bfe52458a38e1d9a71a9e5c677805"
+
+
+def igp_account_data() -> bytes:
+ return base64.b64decode(IGP["dataBase64"])
+
+
+def metadata_from_fixture(*, overhead: bool = True) -> dict[str, str | int | bool]:
+ """Route metadata (veil camelCase keys) read off the fixture's ordered account list (SEALEVEL_NOTES §2)."""
+ accounts = TRANSFER["accounts"]
+ metadata: dict[str, str | int | bool] = {
+ "warpProgramAddress": WARP_PROGRAM_ADDRESS,
+ "tokenPda": accounts[2]["address"],
+ "nativeCollateralPda": accounts[15]["address"],
+ "dispatchAuthorityPda": accounts[5]["address"],
+ "mailboxProgramAddress": accounts[3]["address"],
+ "mailboxOutboxPda": accounts[4]["address"],
+ "igpProgramAddress": accounts[9]["address"],
+ "igpProgramDataPda": accounts[10]["address"],
+ "igpAccount": accounts[13]["address"],
+ "splNoopProgramAddress": accounts[1]["address"],
+ "destinationDomain": ALEO_MAINNET_DOMAIN,
+ "destinationGasAmount": str(DESTINATION_GAS_AMOUNT),
+ "registryCommit": "418056e21734d26a7d14692e0ec5e902cc9e86bf",
+ "solanaReviewedAt": "2026-08-28T00:00:00Z",
+ "solanaConfigSource": "hyperlane-registry@418056e2:deployments/warp_routes/SOL/aleo-config.yaml",
+ }
+ if overhead:
+ metadata["igpOverheadAccount"] = accounts[12]["address"]
+ return metadata
diff --git a/bridge-sdk/tests/fixtures/sealevel-igp-account.json b/bridge-sdk/tests/fixtures/sealevel-igp-account.json
new file mode 100644
index 0000000..d0c1ecf
--- /dev/null
+++ b/bridge-sdk/tests/fixtures/sealevel-igp-account.json
@@ -0,0 +1,5 @@
+{
+ "address": "JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M",
+ "dataBase64": "AUlHUF9fX19f/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYexQI6tUiqLpNeDGtJzI0pBygIJxeztkEnRaP31PGDzh7FAjq1SKouk14Ma0nMjSkHKAgnF7O2QSdFo/fU8YPMqAAAAAQAAAAAuJ3SEEsbS9RMAAAAAAAAAb1OZDAAAAAAAAAAAAAAAABIKAAAAAC4ndIQSxtL1EwAAAAAAAABm4j8FAAAAAAAAAAAAAAAAEjgAAAAArra/JT0xF78GAAAAAAAAAFE/ID4AAAAAAAAAAAAAAAASggAAAAAuJ3SEEsbS9RMAAAAAAAAAZuI/BQAAAAAAAAAAAAAAABKJAAAAAFDOP1FrNzkAAAAAAAAAAAD3+RHS1AEAAAAAAAAAAAAAEo8AAAAA/tlRMWbrDwAAAAAAAAAAAFiBzfhTGgAAAAAAAAAAAAASrQAAAAD87P+Pgp/tAgAAAAAAAAAASRyEIo8AAAAAAAAAAAAAABJxAQAAAHrxgJdMAQAAAAAAAAAAAAAMf+pD85tCAQAAAAAAAAAAEuABAAAALid0hBLG0vUTAAAAAAAAAA6L/xQAAAAAAAAAAAAAAAASxAMAAACWmfc66Ofk3gIAAAAAAAAAAOQLVAIAAAAAAAAAAAAAABLnAwAAAN3ZVj/lDk/RAAAAAAAAAABRSqAAAgAAAAAAAAAAAAAAErUKAAAALid0hBLG0vUTAAAAAAAAAK3y3QsAAAAAAAAAAAAAAAASEhAAAADrjW+d/ma059sCAAAAAAAAkJmSAAAAAAAAAAAAAAAAABLSFAAAAC4ndIQSxtL1EwAAAAAAAAAOi/8UAAAAAAAAAAAAAAAAEvMdAAAAGpTTD9tAAAAAAAAAAAAAAKdz5vFldgYAAAAAAAAAAAASBSEAAAAuJ3SEEsbS9RMAAAAAAAAAZuI/BQAAAAAAAAAAAAAAABIRJgAAAE34OyEArk4AAAAAAAAAAABR4Km4UwUAAAAAAAAAAAAAEiVeAAAAmcZL5bU/AgAAAAAAAAAAAEfUBJxfugAAAAAAAAAAAAASE2MAAAAuJ3SEEsbS9RMAAAAAAAAADov/FAAAAAAAAAAAAAAAABJzgQAAAFPLhMCJpWgAAAAAAAAAAACDQxZUAQQAAAAAAAAAAAAAEouGAAAALid0hBLG0vUTAAAAAAAAAA6L/xQAAAAAAAAAAAAAAAASsZcAAADZmVSdxSUWAAAAAAAAAAAA1RsYp+wSAAAAAAAAAAAAABLjoQAAAPzs/4+Cn+0CAAAAAAAAAABJHIQijwAAAAAAAAAAAAAAErGkAAAALid0hBLG0vUTAAAAAAAAADb+cgkAAAAAAAAAAAAAAAASaqgAAACzdyh4RQBqEwAAAAAAAAAAuFDGlhUAAAAAAAAAAAAAABK4wwAAAPdJMvJaBAQAAAAAAAAAAAD660lY9zoAAAAAAAAAAAAAEi7LAAAA0oEZOMajAAAAAAAAAAAAANixzhcnjwIAAAAAAAAAAAAS8d4AAAAuJ3SEEsbS9RMAAAAAAAAADov/FAAAAAAAAAAAAAAAABII5wAAAC4ndIQSxtL1EwAAAAAAAAAOi/8UAAAAAAAAAAAAAAAAEmCsAgAAnKOeGxC+AwAAAAAAAAAAAFktF27+bwAAAAAAAAAAAAASK1wJAACrKd/7z2QDAAAAAAAAAAAATAUuqH97AAAAAAAAAAAAABLSZwsAAC4ndIQSxtL1EwAAAAAAAACWxgwBAAAAAAAAAAAAAAAAEn8V/AIAodtmGJSxrQAAAAAAAAAAAD8GAAAAAAAAAAAAAAAAAAAJHoRlFQB3Lul3g54ZAAAAAAAAAAAAvluDugYAAAAAAAAAAAAAABJ7gToeAADAKfc9VAUAAAAAAAAAAACDywAAAAAAAAAAAAAAAAAACTLPox4A/Oz/j4Kf7QIAAAAAAAAAAGho3joAAAAAAAAAAAAAAAAS3FNmKwAFPZ1qitLsAAAAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAAZFTEVDANFEje1xBwAAAAAAAAAAAACoFAAAAAAAAAAAAAAAAAAABr2Q+VMA8TQm+7zPggAAAAAAAAAAAIQdAAAAAAAAAAAAAAAAAAAJXGpkYQA0fpEe/NAAAAAAAAAAAAAAx/twl6MDAAAAAAAAAAAAABJvZWxhAGCoIAWvAAAAAAAAAAAAAADhRAEAAAAAAAAAAAAAAAAABnhsb3MAe7XqTBcGAAAAAAAAAAAAAJUtAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "capturedAtSlot": 442410490
+}
diff --git a/bridge-sdk/tests/fixtures/sealevel-transfer-remote.json b/bridge-sdk/tests/fixtures/sealevel-transfer-remote.json
new file mode 100644
index 0000000..37dbfa1
--- /dev/null
+++ b/bridge-sdk/tests/fixtures/sealevel-transfer-remote.json
@@ -0,0 +1,122 @@
+{
+ "signature": "cWFKiumuvVuvrxM8xtunZxNM4FNUppSdyNm7HEqKjV3ZmENebD4DAf44kbyvq9fKJ61VzNrH3tYpLJgUrY8MEGW",
+ "slot": 442407364,
+ "instructionDataBase64": "AQEBAQEBAQEBb2VsYRw0lpkefGEc7V7lzQze6WnFPvyKVJeuBQgZse8A7SkSACqpcJ0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "accounts": [
+ {
+ "address": "11111111111111111111111111111111",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "JDkpV5CsSbhyGhHhirC5DjGPTcuKWUVHtBZ5MFsgu3ZW",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
+ "signer": false,
+ "writable": true
+ },
+ {
+ "address": "ATDttjggAZKyS19kcV6Rn56oMi49gDprZGckRou9vkkY",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "4LZtvKvBAM8Hcf5tuL5R7xYj9JC12v6ho8igDnwzo6WC",
+ "signer": true,
+ "writable": true
+ },
+ {
+ "address": "7H2KAwXsrVWoAhY9ff1nNanYbp4amnF2mZdwzJDi9AhF",
+ "signer": true,
+ "writable": false
+ },
+ {
+ "address": "GttQDgYR9gVLvjMofpVBiU6BgrJp5V7DFWfap6oLV6WY",
+ "signer": false,
+ "writable": true
+ },
+ {
+ "address": "BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho",
+ "signer": false,
+ "writable": true
+ },
+ {
+ "address": "3hWynyfaw9gZxa7ik2vWfYGLp94Vcge3jb7Xa5qZ84k1",
+ "signer": false,
+ "writable": true
+ },
+ {
+ "address": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M",
+ "signer": false,
+ "writable": true
+ },
+ {
+ "address": "11111111111111111111111111111111",
+ "signer": false,
+ "writable": false
+ },
+ {
+ "address": "8HY3hxmnrWwqEmcdwkSnfN9wEQFUkyiwZvU1vMbnXgbC",
+ "signer": false,
+ "writable": true
+ }
+ ],
+ "logMessages": [
+ "Program ComputeBudget111111111111111111111111111111 invoke [1]",
+ "Program ComputeBudget111111111111111111111111111111 success",
+ "Program ComputeBudget111111111111111111111111111111 invoke [1]",
+ "Program ComputeBudget111111111111111111111111111111 success",
+ "Program 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7 invoke [1]",
+ "Program 11111111111111111111111111111111 invoke [2]",
+ "Program 11111111111111111111111111111111 success",
+ "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi invoke [2]",
+ "Program 11111111111111111111111111111111 invoke [3]",
+ "Program 11111111111111111111111111111111 success",
+ "Program log: Protocol fee of 0 paid from 4LZtvKvBAM8Hcf5tuL5R7xYj9JC12v6ho8igDnwzo6WC to BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
+ "Program 11111111111111111111111111111111 invoke [3]",
+ "Program 11111111111111111111111111111111 success",
+ "Program log: Dispatched message to 1634493807, ID 0xffe0409d00c184769b4dfa2a1eaac5a0a79bfe52458a38e1d9a71a9e5c677805",
+ "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi consumed 84675 of 980334 compute units",
+ "Program return: E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi /+BAnQDBhHabTfoqHqrFoKeb/lJFijjh2acanlxneAU=",
+ "Program E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi success",
+ "Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv invoke [2]",
+ "Program 11111111111111111111111111111111 invoke [3]",
+ "Program 11111111111111111111111111111111 success",
+ "Program 11111111111111111111111111111111 invoke [3]",
+ "Program 11111111111111111111111111111111 success",
+ "Program log: Paid IGP JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M for 464000 gas for message 0xffe0\u20267805 to 1634493807",
+ "Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv consumed 105639 of 892647 compute units",
+ "Program BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv success",
+ "Program log: Warp route transfer completed to destination: 1634493807, recipient: 0x1c34\u20262912, remote_amount: 676200000000",
+ "Program 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7 consumed 225867 of 999700 compute units",
+ "Program 8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7 success"
+ ],
+ "amountLamports": 676200000000,
+ "recipientAleoAddress": "aleo1rs6fdxg703s3em27uhxsehhfd8znaly22jt6upggrxc77q8d9yfq33pk28",
+ "senderAddress": "4LZtvKvBAM8Hcf5tuL5R7xYj9JC12v6ho8igDnwzo6WC",
+ "uniqueMessageAddress": "7H2KAwXsrVWoAhY9ff1nNanYbp4amnF2mZdwzJDi9AhF",
+ "lamportDelta": 7023360
+}
diff --git a/bridge-sdk/tests/live/__init__.py b/bridge-sdk/tests/live/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bridge-sdk/tests/live/cases.py b/bridge-sdk/tests/live/cases.py
new file mode 100644
index 0000000..310e765
--- /dev/null
+++ b/bridge-sdk/tests/live/cases.py
@@ -0,0 +1,555 @@
+"""The funded live cases — one function per veil case, driven only by the public ``Bridge`` verbs.
+
+Ported from veil's `test/integration/live/mainnet/*.live.test.ts` (and `testnet/evm-xreserve`):
+five cases, each parametrized over every registry route it covers, each resuming from its own
+state file. The pytest suite and ``scripts/rehearse.py`` both call these functions, so the two can
+never drift.
+
+Shape of every case (veil parity §4):
+
+1. load the state file for this route (fails closed; a completed case re-asserts and returns),
+2. ``quote`` → print the route/amount/fee table → balance precheck (:class:`Underfunded` when the
+ wallet cannot cover it, which callers turn into a skip),
+3. ``execute=False`` stops here — that is veil's ``if (!mainnetExecutionEnabled()) return``,
+4. otherwise ``execute(plan, on_checkpoint=…)`` once, saving the checkpoint to the state file at
+ every boundary, then drop the in-memory progress and ``recover`` from what is on disk,
+5. drive ``wait`` / ``resume`` / ``complete`` until ``next == "done"`` (``failed`` raises),
+6. record the source tx, message id, destination tx and balance delta; mark the state completed.
+
+What this module will never do: retry ``execute`` after a broadcast (ambiguous or not), read or
+set an acknowledgement variable (the caller passes ``execute=``), or print a key, a secret nonce,
+an attestation or a record plaintext.
+"""
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Callable
+
+from aleo_bridge.errors import InsufficientBalanceError
+from aleo_bridge.privacy import record_amount
+from aleo_bridge.registry import Asset, Registry, Route
+from aleo_bridge.units import format_decimal_amount
+
+from .config import one_atomic_unit
+from .helpers import (LiveBenchmark, LiveCaseError, LiveState, Underfunded, ensure_secret_nonce,
+ load_live_state, load_secret_nonce, redacted, save_live_state, wait_for,
+ wait_for_aleo_transaction, wait_for_hyperlane_delivery)
+
+#: ERC-20 ``Transfer(address,address,uint256)``.
+_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
+
+#: veil's per-test budget (`30 * 60_000`).
+CASE_TIMEOUT_SECONDS = 30 * 60.0
+WAIT_TIMEOUT_SECONDS = 20 * 60.0
+WAIT_POLL_SECONDS = 15.0
+_MAX_TRANSITIONS = 12 # a drive loop that cannot settle is a bug, not a slow chain
+
+
+@dataclass(frozen=True)
+class CaseSpec:
+ """One veil case: which routes it covers, what it sends, and how ``execute`` is parametrized."""
+
+ name: str
+ protocol: str # "hyperlane" | "xreserve"
+ source_family: str # "evm" | "aleo" | "solana"
+ mint_mode: str = "public" # "private" for the xReserve private mint into Aleo
+ mode: str | None = None # execute(mode=…): "signer" (Aleo Hyperlane) / "private" (Aleo burn)
+ amount: str | None = None # veil's literal, or None → one atomic unit of the source asset
+ veil_source: str = ""
+
+ @property
+ def private_mint(self) -> bool:
+ return self.mint_mode == "private"
+
+
+CASES: dict[str, CaseSpec] = {
+ "evm-hyperlane": CaseSpec(
+ name="evm-hyperlane", protocol="hyperlane", source_family="evm",
+ veil_source="mainnet/evm-hyperlane.live.test.ts:21-115"),
+ "evm-xreserve": CaseSpec(
+ name="evm-xreserve", protocol="xreserve", source_family="evm", mint_mode="private",
+ amount="2", # veil mainnet/evm-xreserve.live.test.ts:57
+ veil_source="mainnet/evm-xreserve.live.test.ts:31-146"),
+ "aleo-hyperlane": CaseSpec(
+ name="aleo-hyperlane", protocol="hyperlane", source_family="aleo", mode="signer",
+ veil_source="mainnet/aleo-hyperlane.live.test.ts:47-164"),
+ "aleo-xreserve": CaseSpec(
+ name="aleo-xreserve", protocol="xreserve", source_family="aleo", mode="private",
+ amount="2.000001", # veil mainnet/aleo-xreserve.live.test.ts:97
+ veil_source="mainnet/aleo-xreserve.live.test.ts:69-156"),
+ "solana-hyperlane": CaseSpec(
+ name="solana-hyperlane", protocol="hyperlane", source_family="solana",
+ veil_source="mainnet/solana-hyperlane.live.test.ts:20-91"),
+}
+
+CASE_NAMES = tuple(CASES)
+
+
+# ── routes, names, defaults ───────────────────────────────────────────────────
+
+def routes_for_case(registry: Registry, case: str, environment: str = "mainnet") -> list[Route]:
+ """Every registry route *case* covers — both directions are separate cases, so this is one way.
+
+ Includes non-active routes so the caller can report them as skipped-by-registry rather than
+ silently dropping them (veil parity §2).
+ """
+ spec = CASES[case]
+ return [route for route in registry.routes(environment=environment, protocol=spec.protocol)
+ if registry.chain(registry.asset(route.source_asset_id).chain_id).family == spec.source_family]
+
+
+def case_for_route(registry: Registry, route: Route) -> str | None:
+ """The case that covers *route*, or None — 13b asserts no active mainnet route returns None."""
+ family = registry.chain(registry.asset(route.source_asset_id).chain_id).family
+ for spec in CASES.values():
+ if spec.protocol == route.protocol and spec.source_family == family:
+ return spec.name
+ return None
+
+
+def route_slug(route_id: str) -> str:
+ """``hyperlane:ethereum/eth->aleo/eth`` → ``hyperlane-ethereum-eth-aleo-eth`` (veil's state-name rule)."""
+ return re.sub(r"^-|-$", "", re.sub(r"[^a-z0-9]+", "-", route_id, flags=re.IGNORECASE))
+
+
+def state_name(case: str, route_id: str) -> str:
+ """The state file's base name. Always route-qualified, so a case that covers several routes
+ (evm-hyperlane over eth/wbtc/usdt, aleo-hyperlane over eth/wbtc/usdt/sol) can never resume one
+ route's checkpoint under another's — veil only did this for aleo-hyperlane."""
+ return f"{case}-{route_slug(route_id)}"
+
+
+def asset_ref(asset: Asset) -> str:
+ return f"{asset.chain_id}/{asset.key}"
+
+
+def default_amount(registry: Registry, case: str, route: Route) -> str:
+ """veil's literal for the xReserve cases, otherwise one atomic unit of the source asset."""
+ spec = CASES[case]
+ return spec.amount or one_atomic_unit(registry.asset(route.source_asset_id).decimals)
+
+
+def sender_for(bridge: Any, route: Route) -> str | None:
+ """The address that will sign the source leg, read from the configured connection."""
+ family = bridge.registry.chain(bridge.registry.asset(route.source_asset_id).chain_id).family
+ if family == "aleo":
+ return bridge.aleo_address()
+ connection = bridge.ethereum if family == "evm" else bridge.solana
+ if connection is None:
+ raise LiveCaseError(f"No {family} connection is configured for {route.id}")
+ return connection.address
+
+
+def default_recipient(bridge: Any, route: Route) -> str:
+ """Our own address on the destination chain (the operator overrides it through the config vars)."""
+ family = bridge.registry.chain(bridge.registry.asset(route.destination_asset_id).chain_id).family
+ if family == "aleo":
+ return bridge.aleo_address()
+ # Ruling: probe the CONNECTION, never the bridge.eth/bridge.sol properties — those raise.
+ connection = bridge.ethereum if family == "evm" else bridge.solana
+ if connection is None or connection.address is None:
+ raise LiveCaseError(f"No {family} address is configured to receive {route.id}")
+ return connection.address
+
+
+# ── printing (addresses and amounts only, never secrets) ──────────────────────
+
+def _human(bridge: Any, atomic: int, asset_id: str) -> str:
+ return f"{format_decimal_amount(atomic, bridge.registry.asset(asset_id).decimals)} " \
+ f"{bridge.registry.asset(asset_id).symbol}"
+
+
+def print_quote(bridge: Any, quote: Any, *, case: str, route_id: str, log: Callable[[str], None] = print) -> None:
+ """veil's ``console.table``: what is about to move, where, and what it costs."""
+ plan = quote.plan
+ source, destination = bridge.registry.asset(plan.source_asset_id), bridge.registry.asset(plan.destination_asset_id)
+ log(f"\n== {case} [{plan.environment}] {route_id}")
+ log(f" kind {quote.kind} (registry {plan.registry_version})")
+ log(f" amount {plan.amount} {source.symbol} → {quote.amount_out or plan.amount} {destination.symbol}")
+ log(f" sender {plan.sender}")
+ log(f" recipient {plan.recipient}")
+ log(f" mint mode {plan.mint_mode}")
+ for fee in quote.fees:
+ estimated = " (estimated)" if fee.estimated else ""
+ log(f" fee {fee.amount} {bridge.registry.asset(fee.asset_id).symbol} [{fee.kind}]{estimated}")
+ # An EvmXReserveQuote carries no `fees`: its protocol cost is the max fee the deposit authorizes.
+ max_fee = getattr(quote, "max_fee_atomic", None)
+ if max_fee is not None:
+ log(f" fee {_human(bridge, max_fee, source.id)} [xReserve max fee]")
+ for name in ("native_value_atomic", "native_fee_atomic", "approval_required", "total_lamports",
+ "igp_lamports", "rent_lamports", "payment_microcredits", "balance_atomic",
+ "allowance_atomic", "withdrawal_fee_atomic", "max_fee_atomic"):
+ if hasattr(quote, name):
+ log(f" {name:<24}{getattr(quote, name)}")
+ steps = " → ".join(f"{step.id}{'*' if step.irreversible else ''}" for step in plan.steps)
+ log(f" steps {steps} (* irreversible)")
+
+
+# ── balances ──────────────────────────────────────────────────────────────────
+
+def read_balances(bridge: Any) -> dict[str, int]:
+ """``asset_id → atomic`` across every configured chain, or ``{}`` when a chain cannot be read.
+
+ ``Bridge.status()`` already walks exactly the configured connections, so an Aleo-only client
+ simply reports no EVM/Solana rows instead of raising.
+ """
+ try:
+ return {asset_id: atomic for chain in bridge.status().chains for asset_id, atomic in chain.balances.items()}
+ except Exception: # noqa: BLE001 — a precheck must never fail the case
+ return {}
+
+
+def _require(balances: dict[str, int], asset_id: str, needed: int, *, what: str = "balance",
+ log: Callable[[str], None] = print) -> None:
+ have = balances.get(asset_id)
+ if have is None:
+ log(f" {what:<10} {asset_id}: unreadable — proceeding; the chain will enforce it")
+ return
+ log(f" {what:<10} {asset_id}: {have} atomic (need {needed})")
+ if have < needed:
+ raise Underfunded(asset_id=asset_id, needed=needed, have=have, what=what)
+
+
+def _native_asset_id(bridge: Any, chain_id: str) -> str | None:
+ native = [a for a in bridge.registry.assets(chain=chain_id) if a.kind == "native"]
+ return native[0].id if native else None
+
+
+def precheck(bridge: Any, quote: Any, *, case: str, log: Callable[[str], None] = print) -> dict[str, int]:
+ """Refuse to spend what the wallet does not have; raises :class:`Underfunded` with the shortfall."""
+ plan = quote.plan
+ balances = read_balances(bridge)
+ source = bridge.registry.asset(plan.source_asset_id)
+ native_id = _native_asset_id(bridge, source.chain_id)
+
+ if quote.kind == "evm-hyperlane":
+ if source.kind == "native":
+ _require(balances, source.id, quote.native_value_atomic, log=log)
+ else:
+ _require(balances, source.id, plan.amount_atomic, log=log)
+ if native_id:
+ _require(balances, native_id, quote.native_fee_atomic, what="gas", log=log)
+ elif quote.kind == "solana-hyperlane":
+ if native_id:
+ _require(balances, native_id, quote.total_lamports, log=log)
+ elif quote.kind == "aleo-hyperlane":
+ _require(balances, source.id, plan.amount_atomic, log=log)
+ if native_id:
+ _require(balances, native_id, quote.payment_microcredits, what="hook fee", log=log)
+ elif quote.kind == "evm-xreserve":
+ # The quote already read the wallet's USDC balance and allowance on chain.
+ if quote.balance_atomic < plan.amount_atomic:
+ raise Underfunded(asset_id=source.id, needed=plan.amount_atomic, have=quote.balance_atomic)
+ log(f" balance {source.id}: {quote.balance_atomic} atomic (need {plan.amount_atomic})")
+ elif quote.kind == "aleo-xreserve":
+ pass # the private record is selected (and checked) right before the burn
+ return balances
+
+
+# ── the shared engine ─────────────────────────────────────────────────────────
+
+def _saver(state: LiveState, state_path: Path, benchmark: LiveBenchmark) -> Callable[[Any], None]:
+ def save(checkpoint: Any) -> None:
+ state.checkpoint = checkpoint.to_dict() if hasattr(checkpoint, "to_dict") else dict(checkpoint)
+ source = state.checkpoint.get("source") or {}
+ state.source_tx_id = source.get("transactionId") or state.source_tx_id
+ destination = state.checkpoint.get("destination") or {}
+ state.destination_tx_id = destination.get("transactionId") or state.destination_tx_id
+ save_live_state(state_path, state)
+ benchmark.mark("checkpoint-saved")
+ return save
+
+
+def _assert_completed(state: LiveState, case: str) -> LiveState:
+ """Re-running a finished case is a no-op that re-asserts what was recorded (veil parity §4)."""
+ if not state.source_tx_id:
+ raise LiveCaseError(f"{case} state claims completion without a source transaction id")
+ return state
+
+
+def _recover_from_state(bridge: Any, state: LiveState, *, benchmark: LiveBenchmark,
+ log: Callable[[str], None]) -> Any:
+ """Rebuild progress from the checkpoint ON DISK, dropping whatever ``execute`` returned.
+
+ The user's instruction requires the recovery verbs proven for real: the checkpoint the store
+ holds — found through ``bridge.pending()`` when a store is bound — is the only input here.
+ """
+ if state.checkpoint is None:
+ raise LiveCaseError("No checkpoint was saved for this transfer; nothing to recover from")
+ receipt_id = state.checkpoint.get("receiptId")
+ pending = bridge.pending() or []
+ if receipt_id and any(entry.receipt.id == receipt_id for entry in pending):
+ log(f" pending {receipt_id} is in the bound checkpoint store ({len(pending)} in flight)")
+ progress = bridge.recover(state.checkpoint)
+ benchmark.mark("source-recovered")
+ return progress
+
+
+def _drive(bridge: Any, progress: Any, state: LiveState, state_path: Path, *, spec: CaseSpec,
+ secret_nonce: str | None, benchmark: LiveBenchmark, save: Callable[[Any], None],
+ wait_timeout_seconds: float, wait_poll_seconds: float,
+ log: Callable[[str], None]) -> Any:
+ """``wait`` / ``resume`` / ``complete`` until the transfer is done. Never calls ``execute``."""
+ for _ in range(_MAX_TRANSITIONS):
+ if progress.next == "wait":
+ progress = bridge.wait(progress, timeout_seconds=wait_timeout_seconds,
+ poll_seconds=wait_poll_seconds,
+ on_error=lambda exc: log(f" transient {type(exc).__name__}: {exc}"))
+ benchmark.mark("wait-returned")
+ continue
+ if progress.next == "resume":
+ progress = bridge.resume(progress, on_checkpoint=save, secret_nonce=secret_nonce)
+ benchmark.mark("resume-returned")
+ continue
+ if progress.next == "complete":
+ if not secret_nonce:
+ raise LiveCaseError(
+ "The private mint needs the secret nonce kept beside this case's state file; "
+ "it is missing, so the mint cannot be completed here")
+ progress = bridge.complete(progress, secret_nonce=secret_nonce, on_checkpoint=save)
+ benchmark.mark("complete-returned")
+ continue
+ if progress.next == "failed":
+ raise LiveCaseError(f"{spec.name} failed: {progress.error}")
+ if progress.next == "done":
+ return progress
+ raise LiveCaseError(f"Unexpected progress state {progress.next!r} for {spec.name}")
+ raise LiveCaseError(f"{spec.name} did not settle after {_MAX_TRANSITIONS} lifecycle transitions")
+
+
+def _select_private_record(bridge: Any, route: Route, amount_atomic: int,
+ log: Callable[[str], None]) -> str:
+ """The USDCx record the burn will spend, logged by amount and digest — never by plaintext."""
+ program = route.meta_str("remoteToken")
+ try:
+ record = bridge.privacy.select_record(program, amount_atomic)
+ except InsufficientBalanceError as exc:
+ raise Underfunded(asset_id=bridge.registry.asset(route.source_asset_id).id,
+ needed=amount_atomic, have=0, what="private record") from exc
+ log(f" record {program} {redacted([record])} amount={record_amount(record)}")
+ return record
+
+
+def delivery_is_a_balance_rise(route: Route, registry: Registry) -> bool:
+ """True for the Aleo→EVM xReserve withdrawal, the one leg with no delivery query anywhere.
+
+ ``lifecycle.py`` says it in as many words ("xReserve Aleo→EVM: Circle exposes no canonical
+ delivery query") and simply returns the receipt unchanged, so its status stays
+ ``DELIVERY_PENDING`` for ever and ``wait`` can only ever time out. veil does not drive this
+ case to ``done`` either — ``aleo-xreserve.live.test.ts:146-155`` polls the recipient's ERC-20
+ balance until it rises above what it was before the burn, and that is what delivery means here.
+ """
+ if route.protocol != "xreserve":
+ return False
+ source = registry.chain(registry.asset(route.source_asset_id).chain_id).family
+ destination = registry.chain(registry.asset(route.destination_asset_id).chain_id).family
+ return source == "aleo" and destination == "evm"
+
+
+def _wait_for_balance_rise(bridge: Any, asset_id: str, before: int, *, timeout_seconds: float,
+ poll_seconds: float, log: Callable[[str], None]) -> int:
+ """veil ``waitFor(... balanceOf > destinationBalanceBefore)``: the recipient's balance, once it rises."""
+ def read() -> Any:
+ after = read_balances(bridge).get(asset_id)
+ return after if after is not None and after > before else None
+
+ log(f" awaiting {asset_id} to rise above {before} atomic (no delivery query exists for this leg)")
+ return wait_for(read, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds)
+
+
+def _evm_transfer_tx(bridge: Any, asset: Asset, recipient: str, amount_atomic: int,
+ lookback_blocks: int = 5_000) -> str | None:
+ """The transaction that moved *amount_atomic* of *asset* to *recipient*, or None.
+
+ Best effort only: this is a convenience id for the report, so a public RPC that refuses the
+ log range (or returns nothing) leaves ``destinationTxId`` unset rather than failing a leg whose
+ funds have demonstrably arrived.
+ """
+ connection = getattr(bridge, "ethereum", None)
+ if connection is None or asset.locator.kind != "evm-contract":
+ return None
+ try:
+ w3 = connection.w3
+ head = w3.eth.block_number
+ entries = w3.eth.get_logs({
+ "fromBlock": max(head - lookback_blocks, 0), "toBlock": head,
+ "address": w3.to_checksum_address(asset.locator.value),
+ "topics": [_TRANSFER_TOPIC, None, "0x" + "00" * 12 + recipient[2:].lower()]})
+ except Exception: # noqa: BLE001 — a missing id is not a failure
+ return None
+ for entry in reversed(list(entries)):
+ raw = entry["data"]
+ value = int(raw.hex() if hasattr(raw, "hex") else raw, 16)
+ if value == amount_atomic:
+ digest = entry["transactionHash"]
+ return "0x" + (digest.hex() if hasattr(digest, "hex") else str(digest)).removeprefix("0x")
+ return None
+
+
+def run_case(bridge: Any, case: str, route_id: str, *, state_path: Path | str, recipient: str | None = None,
+ amount: str | None = None, execute: bool, benchmark: LiveBenchmark | None = None,
+ wait_timeout_seconds: float = WAIT_TIMEOUT_SECONDS, wait_poll_seconds: float = WAIT_POLL_SECONDS,
+ stop_after_execute: bool = False, log: Callable[[str], None] = print) -> LiveState:
+ """Run one case over one route, resuming from ``state_path``. See the module docstring for the flow.
+
+ ``stop_after_execute=True`` returns as soon as the source transaction is on chain and its
+ checkpoint is on disk, so the caller can throw the whole client away and prove that a *new*
+ ``Bridge`` finishes the transfer from the state file alone. Calling ``run_case`` again with the
+ same ``state_path`` takes the resume branch; ``execute`` is never called twice for one transfer.
+ """
+ spec = CASES[case]
+ state_path = Path(state_path)
+ benchmark = benchmark if benchmark is not None else LiveBenchmark(case, log=log)
+ state = load_live_state(state_path, route_id)
+ if state.completed:
+ log(f" {case} {route_id} is already complete (source {state.source_tx_id})")
+ return _assert_completed(state, case)
+
+ route = bridge.registry.route(route_id)
+ if not route.active:
+ raise LiveCaseError(f"Route {route_id} is {route.availability}; the registry will not execute it")
+ if route.protocol != spec.protocol:
+ raise LiveCaseError(f"Route {route_id} is {route.protocol}, not a {spec.protocol} case")
+ source, destination = bridge.registry.asset(route.source_asset_id), bridge.registry.asset(route.destination_asset_id)
+ amount = amount or default_amount(bridge.registry, case, route)
+ recipient = recipient or default_recipient(bridge, route)
+ sender = sender_for(bridge, route)
+ benchmark.mark("plan-prepared")
+
+ # The private-mint nonce is created only when we are actually going to deposit: a quote-only
+ # rehearsal leaves no secret file behind. An existing one is always reused.
+ secret_nonce = None
+ if spec.private_mint:
+ secret_nonce = ensure_secret_nonce(state_path) if execute else load_secret_nonce(state_path)
+ state.secret_nonce_present = secret_nonce is not None
+
+ progress = None
+ if state.checkpoint is None:
+ quote = bridge.quote(asset_ref(source), asset_ref(destination), amount=amount, recipient=recipient,
+ sender=sender, protocol=route.protocol, mint_mode=spec.mint_mode,
+ secret_nonce=secret_nonce or "0scalar")
+ benchmark.mark("quote-returned")
+ print_quote(bridge, quote, case=case, route_id=route_id, log=log)
+ balances = precheck(bridge, quote, case=case, log=log)
+ before = balances.get(destination.id)
+ if before is not None:
+ state.destination_balance_before = str(before)
+ if not execute:
+ log(f"\n quote only — nothing was submitted for {route_id}.")
+ return state
+ save_live_state(state_path, state)
+
+ record = (_select_private_record(bridge, route, quote.plan.amount_atomic, log)
+ if case == "aleo-xreserve" else None)
+ save = _saver(state, state_path, benchmark)
+ gas = getattr(quote, "payment_microcredits", None) if spec.protocol == "hyperlane" else None
+ progress = bridge.execute(quote.plan, on_checkpoint=save, mode=spec.mode, record=record,
+ secret_nonce=secret_nonce, gas_payment_microcredits=gas)
+ benchmark.mark("execute-returned")
+ state.source_tx_id = progress.receipt.source_tx_id or state.source_tx_id
+ save_live_state(state_path, state)
+ if progress.receipt.protocol_state.get("blockhashExpired") is True:
+ raise LiveCaseError(
+ f"Solana source transaction {state.source_tx_id} expired; inspect it on chain before "
+ "clearing the checkpoint — never re-run execute")
+ if spec.source_family == "aleo" and state.source_tx_id:
+ # veil aleo-hyperlane:153 / aleo-xreserve:136: confirm the source on chain before
+ # recovering, so a rejected execution is reported as itself rather than as a timeout.
+ wait_for_aleo_transaction(bridge, state.source_tx_id)
+ benchmark.mark("source-confirmed")
+ if stop_after_execute:
+ log(f" handover source={state.source_tx_id} checkpoint on disk at {state_path}; "
+ "a new client will recover it")
+ return state
+ else:
+ log(f" resuming {case} {route_id} from the saved checkpoint")
+
+ save = _saver(state, state_path, benchmark)
+ if not execute:
+ # A saved checkpoint plus no acknowledgement: report what is pending, submit nothing.
+ log(f"\n quote only — {route_id} has a saved checkpoint; re-run with the acknowledgement to finish it.")
+ return state
+
+ progress = _recover_from_state(bridge, state, benchmark=benchmark, log=log)
+
+ if delivery_is_a_balance_rise(route, bridge.registry):
+ # No drive loop: `wait` on this leg can only time out (see delivery_is_a_balance_rise).
+ before = int(state.destination_balance_before or 0)
+ after = _wait_for_balance_rise(bridge, destination.id, before, timeout_seconds=wait_timeout_seconds,
+ poll_seconds=wait_poll_seconds, log=log)
+ benchmark.mark("destination-delivered")
+ state.destination_tx_id = state.destination_tx_id or _evm_transfer_tx(
+ bridge, destination, recipient, after - before)
+ log(f" delivered {destination.id} +{after - before} atomic (before {before}, after {after}) "
+ f"tx={state.destination_tx_id}")
+ state.completed = True
+ save_live_state(state_path, state)
+ log(f" done source={state.source_tx_id} destination={state.destination_tx_id}")
+ log(f" {benchmark.summary()}")
+ return state
+
+ progress = _drive(bridge, progress, state, state_path, spec=spec, secret_nonce=secret_nonce,
+ benchmark=benchmark, save=save, wait_timeout_seconds=wait_timeout_seconds,
+ wait_poll_seconds=wait_poll_seconds, log=log)
+ benchmark.mark("destination-delivered")
+
+ receipt = progress.receipt
+ state.source_tx_id = receipt.source_tx_id or state.source_tx_id
+ state.message_id = receipt.protocol_state.get("messageId") or state.message_id
+ state.destination_tx_id = receipt.destination_tx_id or state.destination_tx_id
+ if spec.protocol == "hyperlane" and state.destination_tx_id is None and state.source_tx_id:
+ delivery = wait_for_hyperlane_delivery(state.source_tx_id, timeout_seconds=300, poll_seconds=15)
+ if delivery is not None:
+ state.message_id = state.message_id or delivery.message_id
+ state.destination_tx_id = delivery.destination_tx_id
+ else:
+ log(" explorer unavailable; the destination transaction id was not recorded")
+ state.completed = True
+ save_live_state(state_path, state)
+ after = read_balances(bridge).get(destination.id)
+ if after is not None and state.destination_balance_before is not None:
+ log(f" delivered {destination.id} +{after - int(state.destination_balance_before)} atomic "
+ f"(before {state.destination_balance_before}, after {after})")
+ log(f" done source={state.source_tx_id} message={state.message_id} "
+ f"destination={state.destination_tx_id}")
+ log(f" {benchmark.summary()}")
+ return state
+
+
+def _runner(case: str) -> Callable[..., LiveState]:
+ def run(bridge: Any, route_id: str, *, state_path: Path | str, recipient: str | None = None,
+ amount: str | None = None, execute: bool, benchmark: LiveBenchmark | None = None,
+ **kwargs: Any) -> LiveState:
+ return run_case(bridge, case, route_id, state_path=state_path, recipient=recipient, amount=amount,
+ execute=execute, benchmark=benchmark, **kwargs)
+ run.__name__ = f"run_{case.replace('-', '_')}"
+ run.__doc__ = (f"veil {CASES[case].veil_source}: {case} over one route "
+ f"(amount {CASES[case].amount or 'one atomic unit'}"
+ f"{', private mint' if CASES[case].private_mint else ''}"
+ f"{f', execute mode {CASES[case].mode}' if CASES[case].mode else ''}).")
+ return run
+
+
+run_evm_hyperlane = _runner("evm-hyperlane")
+run_evm_xreserve = _runner("evm-xreserve")
+run_aleo_hyperlane = _runner("aleo-hyperlane")
+run_aleo_xreserve = _runner("aleo-xreserve")
+run_solana_hyperlane = _runner("solana-hyperlane")
+
+RUNNERS: dict[str, Callable[..., LiveState]] = {
+ "evm-hyperlane": run_evm_hyperlane,
+ "evm-xreserve": run_evm_xreserve,
+ "aleo-hyperlane": run_aleo_hyperlane,
+ "aleo-xreserve": run_aleo_xreserve,
+ "solana-hyperlane": run_solana_hyperlane,
+}
+
+__all__ = [
+ "CASES", "CASE_NAMES", "CASE_TIMEOUT_SECONDS", "CaseSpec", "LiveCaseError", "RUNNERS", "Underfunded",
+ "asset_ref", "case_for_route", "default_amount", "default_recipient", "delivery_is_a_balance_rise",
+ "precheck", "print_quote", "read_balances", "route_slug", "routes_for_case", "run_aleo_hyperlane",
+ "run_aleo_xreserve", "run_case", "run_evm_hyperlane", "run_evm_xreserve", "run_solana_hyperlane",
+ "sender_for", "state_name",
+]
diff --git a/bridge-sdk/tests/live/config.py b/bridge-sdk/tests/live/config.py
new file mode 100644
index 0000000..6f1ee9e
--- /dev/null
+++ b/bridge-sdk/tests/live/config.py
@@ -0,0 +1,223 @@
+"""Gates and paths for the funded live cases — a port of veil's `test/integration/live/config.ts`.
+
+Every function here only READS the environment. Nothing in this repository sets, defaults or
+repairs a gate variable: the acknowledgement strings exist so that a human types them into their
+own shell for one command, and an agent that exports them has defeated the only safeguard between
+a test run and somebody's money. Values are never logged — errors name the VARIABLE, never what
+it contained.
+
+Gates (veil config.ts:17-36):
+
+* ``BRIDGE_LIVE_FUNDS=1`` **and** ``BRIDGE_LIVE_STATE_DIR=`` — live-funds
+ tests exist at all.
+* ``BRIDGE_LIVE_MAINNET_ACK=I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS`` **and**
+ ``BRIDGE_LIVE_MAINNET_CASES=`` — the named mainnet case may run.
+* ``BRIDGE_LIVE_MAINNET_EXECUTE=I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS`` — the wallet may
+ actually submit. Without it a case runs to the quote and returns (veil's
+ ``if (!mainnetExecutionEnabled()) return``).
+"""
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+from typing import Mapping
+
+#: The five mainnet cases veil ships, in the order the funding table lists them.
+CASE_NAMES = ("evm-hyperlane", "evm-xreserve", "aleo-hyperlane", "aleo-xreserve", "solana-hyperlane")
+
+FUNDS_VAR = "BRIDGE_LIVE_FUNDS"
+STATE_DIR_VAR = "BRIDGE_LIVE_STATE_DIR"
+MAINNET_ACK_VAR = "BRIDGE_LIVE_MAINNET_ACK"
+MAINNET_ACK = "I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS"
+MAINNET_CASES_VAR = "BRIDGE_LIVE_MAINNET_CASES"
+MAINNET_EXECUTE_VAR = "BRIDGE_LIVE_MAINNET_EXECUTE"
+MAINNET_EXECUTE_ACK = "I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS"
+
+ENVIRONMENTS = ("mainnet", "testnet")
+
+#: Endpoints used when the operator's shell names none. Public, read-mostly, no credentials.
+DEFAULT_ALEO_ENDPOINT = "https://edge.provable.com/api"
+DEFAULT_ETHEREUM_RPC_URL = "https://ethereum-rpc.publicnode.com"
+DEFAULT_SEPOLIA_RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com"
+
+#: Key and RPC variables per environment, most specific first. The live suite resolves connection
+#: material ONLY through these lists, so a testnet run can never pick up the mainnet Aleo key.
+ALEO_KEY_VARS = {
+ "mainnet": ("BRIDGE_PRIVATE_KEY",),
+ "testnet": ("BRIDGE_LIVE_ALEO_TESTNET_PRIVATE_KEY", "ALEO_E2E_PRIVATE_KEY"),
+}
+EVM_KEY_VARS = {
+ "mainnet": ("EVM_PRIVATE_KEY", "BRIDGE_EVM_PRIVATE_KEY"),
+ "testnet": ("BRIDGE_LIVE_EVM_TESTNET_PRIVATE_KEY", "EVM_PRIVATE_KEY", "BRIDGE_EVM_PRIVATE_KEY"),
+}
+EVM_RPC_VARS = {
+ "mainnet": ("ETHEREUM_RPC_URL", "BRIDGE_LIVE_ETHEREUM_RPC_URL"),
+ "testnet": ("SEPOLIA_RPC_URL", "BRIDGE_LIVE_SEPOLIA_RPC_URL"),
+}
+DEFAULT_EVM_RPC_URL = {"mainnet": DEFAULT_ETHEREUM_RPC_URL, "testnet": DEFAULT_SEPOLIA_RPC_URL}
+ALEO_ENDPOINT_VARS = ("BRIDGE_LIVE_ALEO_ENDPOINT", "ALEO_ENDPOINT")
+
+#: The Aleo network name each bridge environment runs on.
+ALEO_NETWORKS = {"mainnet": "mainnet", "testnet": "testnet"}
+
+#: Recipient overrides per destination-chain family; the default is our own address on that chain.
+RECIPIENT_VARS = {
+ "aleo": "BRIDGE_LIVE_ALEO_MAINNET_RECIPIENT",
+ "evm": "BRIDGE_LIVE_EVM_RECIPIENT",
+ "solana": "BRIDGE_LIVE_SOLANA_RECIPIENT",
+}
+
+_EVM_KEY_RE = re.compile(r"^[0-9a-f]{64}$", re.IGNORECASE)
+
+
+class LiveConfigError(Exception):
+ """A live case was explicitly enabled but is not configured (never carries a secret value)."""
+
+
+def _env(env: Mapping[str, str] | None) -> Mapping[str, str]:
+ return os.environ if env is None else env
+
+
+def value(name: str, env: Mapping[str, str] | None = None) -> str | None:
+ """The trimmed value of *name*, or None when unset or blank. Never logs."""
+ raw = _env(env).get(name)
+ trimmed = raw.strip() if isinstance(raw, str) else None
+ return trimmed or None
+
+
+def required(name: str, env: Mapping[str, str] | None = None) -> str:
+ """veil ``required()``: the value of *name*, or a clear error naming only the variable."""
+ found = value(name, env)
+ if not found:
+ raise LiveConfigError(f"Missing {name}; the live bridge case was explicitly enabled but is not configured")
+ return found
+
+
+def required_evm_private_key(name: str, env: Mapping[str, str] | None = None) -> str:
+ """veil ``requiredEvmPrivateKey()``: one 32-byte key normalised to ``0x…``; the value never appears."""
+ raw = required(name, env)
+ body = raw[2:] if raw[:2].lower() == "0x" else raw
+ if not _EVM_KEY_RE.match(body):
+ raise LiveConfigError(f"{name} must contain exactly 32 hexadecimal bytes")
+ return f"0x{body.lower()}"
+
+
+def live_funds_enabled(env: Mapping[str, str] | None = None) -> bool:
+ """veil ``liveFundsEnabled()``: funded cases exist at all (flag exactly ``"1"`` + a state dir)."""
+ source = _env(env)
+ return source.get(FUNDS_VAR) == "1" and bool(value(STATE_DIR_VAR, env))
+
+
+def mainnet_case_enabled(name: str, env: Mapping[str, str] | None = None) -> bool:
+ """veil ``mainnetCaseEnabled()``: funds gate + the exact acknowledgement + *name* in the case list."""
+ if not live_funds_enabled(env):
+ return False
+ if _env(env).get(MAINNET_ACK_VAR) != MAINNET_ACK:
+ return False
+ listed = {entry.strip() for entry in (value(MAINNET_CASES_VAR, env) or "").split(",")}
+ return name in (listed - {""})
+
+
+def mainnet_execution_enabled(env: Mapping[str, str] | None = None) -> bool:
+ """veil ``mainnetExecutionEnabled()``: the wallet may submit. Read here, typed by a human elsewhere."""
+ return _env(env).get(MAINNET_EXECUTE_VAR) == MAINNET_EXECUTE_ACK
+
+
+def one_atomic_unit(decimals: int) -> str:
+ """veil ``oneAtomicUnit()``: the smallest positive display amount of an asset ("0.000001" at 6)."""
+ if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0:
+ raise LiveConfigError(f"Invalid asset decimals: {decimals!r}")
+ return "1" if decimals == 0 else f"0.{'0' * (decimals - 1)}1"
+
+
+def state_dir(env: Mapping[str, str] | None = None) -> Path:
+ """``BRIDGE_LIVE_STATE_DIR`` as a path — the operator's own directory, outside this repository."""
+ return Path(required(STATE_DIR_VAR, env)).expanduser()
+
+
+def live_state_path(environment: str, name: str, env: Mapping[str, str] | None = None) -> Path:
+ """veil ``liveStatePath()``: ``//.json``."""
+ if environment not in ENVIRONMENTS:
+ raise LiveConfigError(f"environment must be one of {ENVIRONMENTS}, got {environment!r}")
+ return state_dir(env) / environment / f"{name}.json"
+
+
+def case_route_override(case: str, env: Mapping[str, str] | None = None) -> str | None:
+ """``BRIDGE_LIVE__ROUTE_ID`` (veil's per-case route override), or None."""
+ return value(f"BRIDGE_LIVE_{case.replace('-', '_').upper()}_ROUTE_ID", env)
+
+
+def case_amount_override(case: str, env: Mapping[str, str] | None = None) -> str | None:
+ """``BRIDGE_LIVE__AMOUNT``, or ``BRIDGE_LIVE_XRESERVE_AMOUNT`` for either xReserve case."""
+ specific = value(f"BRIDGE_LIVE_{case.replace('-', '_').upper()}_AMOUNT", env)
+ if specific:
+ return specific
+ return value("BRIDGE_LIVE_XRESERVE_AMOUNT", env) if case.endswith("xreserve") else None
+
+
+def _environment(environment: str) -> str:
+ if environment not in ENVIRONMENTS:
+ raise LiveConfigError(f"environment must be one of {ENVIRONMENTS}, got {environment!r}")
+ return environment
+
+
+def first_value(names: tuple[str, ...], env: Mapping[str, str] | None = None) -> tuple[str, str] | None:
+ """The first of *names* that is set, as ``(variable, value)`` — or None. Never logs the value."""
+ for name in names:
+ found = value(name, env)
+ if found:
+ return name, found
+ return None
+
+
+def aleo_private_key(environment: str, env: Mapping[str, str] | None = None) -> str:
+ """The Aleo key for *environment* (``BRIDGE_PRIVATE_KEY`` on mainnet, ``ALEO_E2E_PRIVATE_KEY``
+ — or its ``BRIDGE_LIVE_ALEO_TESTNET_PRIVATE_KEY`` alias — on testnet)."""
+ names = ALEO_KEY_VARS[_environment(environment)]
+ found = first_value(names, env)
+ if found is None:
+ raise LiveConfigError(f"Missing {' / '.join(names)}; the {environment} live case needs an Aleo key")
+ return found[1]
+
+
+def evm_private_key(environment: str, env: Mapping[str, str] | None = None) -> str:
+ """The EVM key for *environment*, normalised to ``0x…`` (the value never appears in an error)."""
+ names = EVM_KEY_VARS[_environment(environment)]
+ found = first_value(names, env)
+ if found is None:
+ raise LiveConfigError(f"Missing {' / '.join(names)}; the {environment} live case needs an EVM key")
+ return required_evm_private_key(found[0], env)
+
+
+def evm_rpc_url(environment: str, env: Mapping[str, str] | None = None) -> str:
+ """The Ethereum (mainnet) or Sepolia (testnet) RPC url, falling back to the public default."""
+ found = first_value(EVM_RPC_VARS[_environment(environment)], env)
+ return found[1] if found else DEFAULT_EVM_RPC_URL[environment]
+
+
+def aleo_endpoint(env: Mapping[str, str] | None = None) -> str:
+ """The Aleo API root, falling back to the open, credential-free edge host."""
+ found = first_value(ALEO_ENDPOINT_VARS, env)
+ return found[1] if found else DEFAULT_ALEO_ENDPOINT
+
+
+def recipient_override(family: str, env: Mapping[str, str] | None = None) -> str | None:
+ """The operator's recipient override for a destination-chain *family*, or None (use our own address)."""
+ try:
+ name = RECIPIENT_VARS[family]
+ except KeyError:
+ raise LiveConfigError(f"No recipient override variable for chain family {family!r}") from None
+ return value(name, env)
+
+
+__all__ = [
+ "ALEO_ENDPOINT_VARS", "ALEO_KEY_VARS", "ALEO_NETWORKS", "CASE_NAMES", "DEFAULT_ALEO_ENDPOINT",
+ "DEFAULT_ETHEREUM_RPC_URL", "DEFAULT_EVM_RPC_URL", "DEFAULT_SEPOLIA_RPC_URL", "ENVIRONMENTS",
+ "EVM_KEY_VARS", "EVM_RPC_VARS", "FUNDS_VAR", "LiveConfigError", "MAINNET_ACK", "MAINNET_ACK_VAR",
+ "MAINNET_CASES_VAR", "MAINNET_EXECUTE_ACK", "MAINNET_EXECUTE_VAR", "RECIPIENT_VARS", "STATE_DIR_VAR",
+ "aleo_endpoint", "aleo_private_key", "case_amount_override", "case_route_override", "evm_private_key",
+ "evm_rpc_url", "first_value", "live_funds_enabled", "live_state_path", "mainnet_case_enabled",
+ "mainnet_execution_enabled", "one_atomic_unit", "recipient_override", "required",
+ "required_evm_private_key", "state_dir", "value",
+]
diff --git a/bridge-sdk/tests/live/conftest.py b/bridge-sdk/tests/live/conftest.py
new file mode 100644
index 0000000..b196241
--- /dev/null
+++ b/bridge-sdk/tests/live/conftest.py
@@ -0,0 +1,17 @@
+import os
+
+import pytest
+
+from aleo_bridge import Bridge
+
+
+@pytest.fixture(scope="session")
+def live_bridge() -> Bridge:
+ if os.environ.get("BRIDGE_LIVE_READS") != "1":
+ pytest.skip("set BRIDGE_LIVE_READS=1 (and BRIDGE_PRIVATE_KEY) to run read-only live checks")
+ if not os.environ.get("BRIDGE_PRIVATE_KEY"):
+ pytest.skip("BRIDGE_PRIVATE_KEY is required for Bridge.from_env()")
+ bridge = Bridge.from_env()
+ if bridge.environment != "mainnet":
+ pytest.skip("live read checks target the mainnet routes; unset ALEO_NETWORK or set it to mainnet")
+ return bridge
diff --git a/bridge-sdk/tests/live/helpers.py b/bridge-sdk/tests/live/helpers.py
new file mode 100644
index 0000000..7628a3b
--- /dev/null
+++ b/bridge-sdk/tests/live/helpers.py
@@ -0,0 +1,393 @@
+"""Persistence, polling and timing for the funded live cases — a port of veil's
+`test/integration/live/helpers.ts` (plus the secret-nonce file veil does not have).
+
+Nothing here signs or submits anything. What it does own is the memory of a run: the state file
+that lets a case resume across processes, the private-mint secret that must survive a crash but
+must never reach a log, a checkpoint or the state JSON, and the read-only lookups that answer
+"did it arrive?".
+
+State files are veil-shaped on disk (camelCase keys, one JSON object per case) and fail CLOSED:
+a corrupt or wrong-route file raises rather than quietly starting a fresh transfer over funds that
+may already be in flight.
+"""
+from __future__ import annotations
+
+import json
+import os
+import time
+from dataclasses import asdict, dataclass, field
+from pathlib import Path
+from typing import Any, Callable, Iterable
+
+from aleo_bridge._base58 import b58decode
+from aleo_bridge.encoding import validate_scalar
+from aleo_bridge.lifecycle import aleo_transaction_status
+
+#: veil helpers.ts:85 — the Hyperlane explorer's public GraphQL endpoint (read-only).
+HYPERLANE_EXPLORER_URL = "https://explorer4.hasura.app/v1/graphql"
+HYPERLANE_QUERY = """query ByOrigin($hash: bytea!) {
+ message_view(where: {origin_tx_hash: {_eq: $hash}}, limit: 1) {
+ msg_id is_delivered destination_tx_hash
+ }
+}"""
+
+DEFAULT_TIMEOUT_SECONDS = 1200.0 # veil helpers.ts:70 — 20 minutes
+DEFAULT_POLL_SECONDS = 15.0
+
+_SECRET_SUFFIX = ".secret"
+_STATE_FIELDS = {
+ "route_id": "routeId",
+ "source_tx_id": "sourceTxId",
+ "message_id": "messageId",
+ "destination_tx_id": "destinationTxId",
+ "destination_balance_before": "destinationBalanceBefore",
+ "completed": "completed",
+ "checkpoint": "checkpoint",
+ "secret_nonce_present": "secretNoncePresent",
+}
+_STRING_FIELDS = ("source_tx_id", "message_id", "destination_tx_id", "destination_balance_before")
+
+
+class LiveStateError(Exception):
+ """A state file could not be trusted: corrupt, malformed, or bound to another route."""
+
+
+class LiveTimeoutError(Exception):
+ """A live verification ran out of time; the transfer is still in flight (never a failure verdict)."""
+
+
+class ExplorerError(Exception):
+ """The Hyperlane explorer answered with a GraphQL error rather than data."""
+
+
+class LiveCaseError(Exception):
+ """A live case cannot continue (a rejected source transaction, an expired blockhash, …)."""
+
+
+class Underfunded(Exception):
+ """A wallet cannot cover amount + fees. Callers turn this into a skip, printing the shortfall."""
+
+ def __init__(self, *, asset_id: str, needed: int, have: int, what: str = "balance") -> None:
+ self.asset_id, self.needed, self.have, self.what = asset_id, needed, have, what
+ self.shortfall = max(needed - have, 0)
+ super().__init__(f"Insufficient {asset_id} {what}: need {needed} atomic units, have {have} "
+ f"(short {self.shortfall})")
+
+
+# ── state files ───────────────────────────────────────────────────────────────
+
+@dataclass
+class LiveState:
+ """veil ``LiveState`` + ``secretNoncePresent``.
+
+ The private-mint nonce itself is NOT a field: it lives in the sibling ``.secret`` file
+ (mode 0600) and only its presence is recorded here, so a state file can be pasted into a bug
+ report without leaking the commitment secret.
+ """
+
+ route_id: str
+ source_tx_id: str | None = None
+ message_id: str | None = None
+ destination_tx_id: str | None = None
+ destination_balance_before: str | None = None
+ completed: bool = False
+ checkpoint: dict[str, Any] | None = None
+ secret_nonce_present: bool = False
+
+ def to_dict(self) -> dict[str, Any]:
+ """veil's on-disk shape: camelCase keys, unset optionals omitted."""
+ raw = asdict(self)
+ out: dict[str, Any] = {}
+ for attribute, key in _STATE_FIELDS.items():
+ value = raw[attribute]
+ if value is None or value is False:
+ continue
+ out[key] = value
+ out["routeId"] = self.route_id
+ return out
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "LiveState":
+ return cls(**{attribute: data.get(key) for attribute, key in _STATE_FIELDS.items()
+ if data.get(key) is not None})
+
+
+def _validate(state: LiveState, route_id: str, path: Path) -> LiveState:
+ if state.route_id != route_id:
+ raise LiveStateError(f"Live state route {state.route_id!r} does not match {route_id!r}: {path}")
+ for attribute in _STRING_FIELDS:
+ value = getattr(state, attribute)
+ if value is not None and not isinstance(value, str):
+ raise LiveStateError(f"Live state {_STATE_FIELDS[attribute]} is invalid: {path}")
+ if not isinstance(state.completed, bool):
+ raise LiveStateError(f"Live state completed flag is invalid: {path}")
+ if not isinstance(state.secret_nonce_present, bool):
+ raise LiveStateError(f"Live state secretNoncePresent flag is invalid: {path}")
+ if state.checkpoint is not None and not isinstance(state.checkpoint, dict):
+ raise LiveStateError(f"Live state checkpoint is invalid: {path}")
+ return state
+
+
+def load_live_state(path: Path | str, route_id: str) -> LiveState:
+ """veil ``loadLiveState()``: the saved state for *route_id*, or an empty one when absent.
+
+ Fails closed on corrupt JSON, a non-object payload, a wrong route or a malformed field — a
+ state file we cannot read is never treated as "no transfer in flight".
+ """
+ target = Path(path)
+ try:
+ raw = target.read_text(encoding="utf-8")
+ except FileNotFoundError:
+ return LiveState(route_id=route_id)
+ try:
+ parsed = json.loads(raw)
+ except json.JSONDecodeError as exc:
+ raise LiveStateError(f"Live state is not valid JSON: {target} ({exc})") from exc
+ if not isinstance(parsed, dict):
+ raise LiveStateError(f"Live state is not an object: {target}")
+ if not isinstance(parsed.get("routeId"), str):
+ raise LiveStateError(f"Live state routeId is invalid: {target}")
+ return _validate(LiveState.from_dict(parsed), route_id, target)
+
+
+def save_live_state(path: Path | str, state: LiveState) -> Path:
+ """veil ``saveLiveState()``: atomic (temp file + ``os.replace``), mode 0600, parents 0700."""
+ target = Path(path)
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
+ temporary = target.with_name(f".{target.name}.tmp")
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+ try:
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
+ handle.write(json.dumps(state.to_dict(), indent=2, sort_keys=True))
+ handle.write("\n")
+ os.chmod(temporary, 0o600)
+ os.replace(temporary, target)
+ except BaseException:
+ try:
+ os.unlink(temporary)
+ except FileNotFoundError:
+ pass
+ raise
+ return target
+
+
+# ── the private-mint secret nonce (beside the state, never inside it) ─────────
+
+def generate_secret_nonce() -> str:
+ """A fresh Aleo scalar literal for one private mint (248 random bits, below the scalar modulus)."""
+ import secrets
+
+ return validate_scalar(f"{secrets.randbits(248)}scalar")
+
+
+def secret_path(state_path: Path | str) -> Path:
+ """``.secret`` — the sibling file that holds the nonce for that case."""
+ target = Path(state_path)
+ return target.with_name(target.name + _SECRET_SUFFIX)
+
+
+def save_secret_nonce(state_path: Path | str, nonce: str) -> Path:
+ """Write *nonce* exclusively (``O_CREAT|O_EXCL``, 0600). An existing file raises rather than being
+ overwritten: the nonce of a deposit already on chain is the only way to finish that mint."""
+ validate_scalar(nonce)
+ target = secret_path(state_path)
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
+ fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
+ handle.write(f"{nonce}\n")
+ return target
+
+
+def load_secret_nonce(state_path: Path | str) -> str | None:
+ """The kept nonce for this case, or None when there is no secret file."""
+ try:
+ text = secret_path(state_path).read_text(encoding="utf-8").strip()
+ except FileNotFoundError:
+ return None
+ return validate_scalar(text)
+
+
+def ensure_secret_nonce(state_path: Path | str) -> str:
+ """The case's nonce: the kept one if the secret file exists, otherwise a fresh one, saved once."""
+ existing = load_secret_nonce(state_path)
+ if existing is not None:
+ return existing
+ nonce = generate_secret_nonce()
+ save_secret_nonce(state_path, nonce)
+ return nonce
+
+
+# ── benchmark marks ───────────────────────────────────────────────────────────
+
+@dataclass(frozen=True)
+class Mark:
+ step: str
+ elapsed_ms: int
+ total_ms: int
+
+
+@dataclass
+class LiveBenchmark:
+ """veil ``createLiveBenchmark()``: per-step and total elapsed milliseconds, printed as they happen.
+
+ ``marks`` is kept so a pytest case can hand the whole timeline to ``record_property``.
+ """
+
+ label: str
+ now: Callable[[], float] = time.monotonic
+ log: Callable[[str], None] = print
+ marks: list[Mark] = field(default_factory=list)
+
+ def __post_init__(self) -> None:
+ self._started = self.now()
+ self._previous = self._started
+
+ def mark(self, step: str) -> Mark:
+ current = self.now()
+ entry = Mark(step, round((current - self._previous) * 1000), round((current - self._started) * 1000))
+ self._previous = current
+ self.marks.append(entry)
+ self.log(f"[{self.label}] {step}: +{entry.elapsed_ms}ms (total {entry.total_ms}ms)")
+ return entry
+
+ def summary(self) -> str:
+ if not self.marks:
+ return f"{self.label}: no marks"
+ steps = ", ".join(f"{m.step} +{m.elapsed_ms}ms" for m in self.marks)
+ return f"{self.label}: {steps} (total {self.marks[-1].total_ms}ms)"
+
+ def as_dict(self) -> dict[str, int]:
+ return {m.step: m.elapsed_ms for m in self.marks}
+
+
+# ── polling ───────────────────────────────────────────────────────────────────
+
+def wait_for(read: Callable[[], Any], *, timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
+ poll_seconds: float = DEFAULT_POLL_SECONDS, sleep: Callable[[float], None] = time.sleep,
+ now: Callable[[], float] = time.monotonic) -> Any:
+ """veil ``waitFor()``: poll *read* until it returns something that is not None.
+
+ Always reads at least once, then checks the deadline — so a zero timeout still performs one
+ read. A timeout raises :class:`LiveTimeoutError`; the transfer is not failed, it is unfinished,
+ and the state file on disk is how you resume it.
+ """
+ deadline = now() + timeout_seconds
+ while True:
+ value = read()
+ if value is not None:
+ return value
+ if now() >= deadline:
+ raise LiveTimeoutError(
+ "Live bridge verification timed out; the transfer is still in flight — "
+ "resume from the persisted state file")
+ sleep(poll_seconds)
+
+
+# ── Hyperlane explorer (read-only HTTP) ───────────────────────────────────────
+
+@dataclass(frozen=True)
+class HyperlaneDelivery:
+ message_id: str
+ destination_tx_id: str
+
+
+def _post(url: str, payload: dict[str, Any], timeout: float) -> Any:
+ import requests
+
+ return requests.post(url, json=payload, timeout=timeout,
+ headers={"content-type": "application/json"})
+
+
+def _bytea(source_tx_id: str) -> str:
+ """veil helpers.ts:81-83: an EVM ``0x`` hash or a Solana base58 signature → PostgreSQL bytea."""
+ if source_tx_id.startswith("0x"):
+ return f"\\x{source_tx_id[2:]}"
+ return f"\\x{b58decode(source_tx_id).hex()}"
+
+
+def _normalize(value: str) -> str:
+ return f"0x{value[2:]}" if value.startswith("\\x") else value
+
+
+def hyperlane_delivery(source_tx_id: str, *, post: Callable[..., Any] = _post,
+ timeout: float = 30.0) -> HyperlaneDelivery | None:
+ """One read of the Hyperlane explorer for the message dispatched by *source_tx_id*.
+
+ Returns None while the message is undelivered AND when the explorer is throttled or broken
+ (HTTP 429/5xx, or an unreachable host): a rate-limited explorer says nothing about the
+ transfer. A GraphQL error is a bug in the query and is raised (veil helpers.ts:104-106).
+ """
+ payload = {"query": HYPERLANE_QUERY, "variables": {"hash": _bytea(source_tx_id)}}
+ try:
+ response = post(HYPERLANE_EXPLORER_URL, payload, timeout)
+ except Exception: # noqa: BLE001 — network flake, never a verdict
+ return None
+ status = getattr(response, "status_code", 200)
+ if status == 429 or status >= 500:
+ return None
+ if status >= 400:
+ raise ExplorerError(f"Hyperlane explorer returned HTTP {status}")
+ body = response.json()
+ errors = body.get("errors") if isinstance(body, dict) else None
+ if errors:
+ joined = "; ".join(str(e.get("message", "unknown error")) for e in errors)
+ raise ExplorerError(f"Hyperlane explorer query failed: {joined}")
+ rows = ((body.get("data") or {}).get("message_view") or []) if isinstance(body, dict) else []
+ message = rows[0] if rows else None
+ if not message or not message.get("is_delivered") or not message.get("msg_id") \
+ or not message.get("destination_tx_hash"):
+ return None
+ return HyperlaneDelivery(message_id=_normalize(message["msg_id"]),
+ destination_tx_id=_normalize(message["destination_tx_hash"]))
+
+
+def wait_for_hyperlane_delivery(source_tx_id: str, *, timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
+ poll_seconds: float = DEFAULT_POLL_SECONDS,
+ sleep: Callable[[float], None] = time.sleep,
+ now: Callable[[], float] = time.monotonic,
+ post: Callable[..., Any] = _post) -> HyperlaneDelivery | None:
+ """Poll the explorer for the destination transaction id, or give up quietly.
+
+ This lookup is a convenience on top of a leg the SDK has already called ``done``, so a timeout
+ or a throttled explorer returns None instead of failing the case (veil parity §8).
+ """
+ try:
+ return wait_for(lambda: hyperlane_delivery(source_tx_id, post=post),
+ timeout_seconds=timeout_seconds, poll_seconds=poll_seconds, sleep=sleep, now=now)
+ except LiveTimeoutError:
+ return None
+
+
+# ── Aleo confirmation ─────────────────────────────────────────────────────────
+
+def wait_for_aleo_transaction(bridge: Any, tx_id: str, *, timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
+ poll_seconds: float = DEFAULT_POLL_SECONDS,
+ sleep: Callable[[float], None] = time.sleep,
+ now: Callable[[], float] = time.monotonic) -> None:
+ """veil ``waitForAleoTransaction()``: block until *tx_id* is accepted; a rejection raises."""
+
+ def read() -> Any:
+ status, error = aleo_transaction_status(bridge, tx_id)
+ if status == "rejected":
+ raise LiveCaseError(error or f"Aleo transaction {tx_id} was rejected")
+ return True if status == "accepted" else None
+
+ wait_for(read, timeout_seconds=timeout_seconds, poll_seconds=poll_seconds, sleep=sleep, now=now)
+
+
+def redacted(values: Iterable[str]) -> str:
+ """A stable, non-reversible tag for a secret-bearing string (a record plaintext), for logs."""
+ import hashlib
+
+ digest = hashlib.sha256("".join(values).encode("utf-8")).hexdigest()
+ return f"sha256:{digest[:12]}"
+
+
+__all__ = [
+ "DEFAULT_POLL_SECONDS", "DEFAULT_TIMEOUT_SECONDS", "ExplorerError", "HYPERLANE_EXPLORER_URL",
+ "HYPERLANE_QUERY", "HyperlaneDelivery", "LiveBenchmark", "LiveCaseError", "LiveState",
+ "LiveStateError", "LiveTimeoutError", "Mark", "Underfunded", "ensure_secret_nonce",
+ "generate_secret_nonce", "hyperlane_delivery", "load_live_state", "load_secret_nonce", "redacted",
+ "save_live_state", "save_secret_nonce", "secret_path", "wait_for", "wait_for_aleo_transaction",
+ "wait_for_hyperlane_delivery",
+]
diff --git a/bridge-sdk/tests/live/test_aleo_reads.py b/bridge-sdk/tests/live/test_aleo_reads.py
new file mode 100644
index 0000000..df483ff
--- /dev/null
+++ b/bridge-sdk/tests/live/test_aleo_reads.py
@@ -0,0 +1,138 @@
+"""Read-only checks against mainnet: IGP quotes, mailbox/nullifier reads, registry-vs-chain drift, mapping names.
+A drift failure means the deployment moved since veil's review — STOP and report; never edit the registry to pass."""
+import os
+import re
+
+import pytest
+
+from aleo_bridge import encoding as enc
+from aleo_bridge.client import BALANCE_MAPPING, balance_program
+from aleo_bridge.freezelist import CURRENT_ROOT_KEY, FREEZE_LIST_LAST_INDEX_MAPPING, FREEZE_LIST_MAPPING, build_tree, generate_leaves
+from aleo_bridge.hyperlane import compute_gas_payment, gas_config_key, parse_gas_config
+
+pytestmark = pytest.mark.live
+
+MESSAGE_ID = "0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d"
+ALEO_ORIGIN = ["aleo/eth", "aleo/wbtc", "aleo/usdt", "aleo/sol"]
+
+
+def _squash(text: str) -> str:
+ return re.sub(r"\s+", "", text)
+
+
+@pytest.mark.parametrize("asset", ALEO_ORIGIN)
+def test_igp_quote_is_a_positive_u64(live_bridge, asset):
+ quote = live_bridge.hyperlane.quote_gas_payment(asset)
+ route = live_bridge.hyperlane.outbound_route(asset)
+ assert quote.route_id == route.id
+ assert 0 < quote.payment_microcredits < 2**64 and quote.gas_price > 0 and quote.exchange_rate > 0
+ assert quote.gas_limit == int(route.metadata["aleoRemoteRouterGas"])
+ # Self-consistency: recompute the exact u64 from a raw mapping read taken independently of quote_gas_payment.
+ literal = live_bridge.mapping_value(route.meta_str("aleoHookManagerProgram"), "destination_gas_configs",
+ gas_config_key(route))
+ assert literal is not None, f"destination_gas_configs is missing on chain for {route.id}"
+ config = parse_gas_config(literal)
+ assert config == {"gas_overhead": quote.gas_overhead, "gas_price": quote.gas_price, "exchange_rate": quote.exchange_rate}
+ recomputed = compute_gas_payment(gas_limit=quote.gas_limit, gas_overhead=config["gas_overhead"],
+ gas_price=config["gas_price"], exchange_rate=config["exchange_rate"])
+ assert recomputed == quote.payment_microcredits
+
+
+def test_mailbox_deliveries_vector(live_bridge, record_property):
+ delivered = live_bridge.hyperlane.is_delivered(MESSAGE_ID)
+ record_property("hyperlane_is_delivered_pinned_message", delivered)
+ assert delivered is True
+ assert live_bridge.hyperlane.is_delivered("0x" + "00" * 32) is False
+
+
+def test_usdcx_bridge_nullifier_read(live_bridge):
+ assert live_bridge.xreserve.is_delivered(bytes(32)) is False # the zero nonce was never deposited
+ assert live_bridge.xreserve.inbound_route().metadata["bridgeProgram"] == "usdcx_bridge_v2.aleo"
+
+
+@pytest.mark.parametrize("asset", ALEO_ORIGIN)
+def test_registry_matches_deployed_warp_route_state(live_bridge, asset, record_property):
+ route = live_bridge.hyperlane.outbound_route(asset)
+ program = route.metadata["aleoRouterProgram"]
+ app = _squash(live_bridge.mapping_value(program, "app_metadata", "true") or "")
+ assert f"token_owner:{route.metadata['aleoTokenOwner']}" in app
+ assert f"token_id:{route.metadata['aleoTokenId']}" in app
+ assert f"local_decimals:{route.metadata['aleoLocalDecimals']}u8" in app
+ assert f"remote_decimals:{route.metadata['aleoRemoteDecimals']}u8" in app
+ raw_router = live_bridge.mapping_value(program, "remote_routers", f"{route.metadata['aleoDestinationDomain']}u32")
+ record_property(f"remote_routers_{asset}", raw_router)
+ if raw_router is None:
+ pytest.skip(f"{program}/remote_routers[{route.metadata['aleoDestinationDomain']}u32] did not parse; logged as None")
+ router = _squash(raw_router)
+ assert f"gas:{route.metadata['aleoRemoteRouterGas']}u128" in router
+ assert _squash(route.metadata["aleoRemoteRouterRecipient"]) in router
+
+
+def test_mailbox_state_matches_registry(live_bridge):
+ route = live_bridge.hyperlane.outbound_route("aleo/eth")
+ mailbox = _squash(live_bridge.mapping_value(route.metadata["aleoMailboxProgram"], "mailbox", "true") or "")
+ assert f"default_hook:{route.metadata['aleoMailboxDefaultHook']}" in mailbox
+ assert f"required_hook:{route.metadata['aleoMailboxRequiredHook']}" in mailbox
+
+
+def test_freeze_list_mappings_exist_and_proof_builds(live_bridge):
+ program_id = "usdcx_stablecoin.aleo"
+ fl_program = live_bridge.freezelist.freeze_list_program(program_id)
+ assert fl_program == "usdcx_freezelist.aleo" # the freeze-list mappings live on the freezelist program, not the token
+ names = live_bridge.program(fl_program).mappings()
+ assert FREEZE_LIST_MAPPING in names and FREEZE_LIST_LAST_INDEX_MAPPING in names, \
+ f"freeze-list mapping names differ on chain: {sorted(names)} — update the two constants in freezelist.py only"
+ leaves = live_bridge.freezelist.leaves(program_id)
+ if "freeze_list_root" in names:
+ root = build_tree(generate_leaves(leaves), "mainnet")[-1]
+ on_chain_root = live_bridge.mapping_value(fl_program, "freeze_list_root", CURRENT_ROOT_KEY)
+ assert on_chain_root == f"{root}field", \
+ f"leaves={leaves!r} recomputed root {root} != on-chain freeze_list_root[{CURRENT_ROOT_KEY}] {on_chain_root!r}"
+ # exclusion_proof() independently re-verifies the same on-chain root before building the proof.
+ proof = live_bridge.freezelist.exclusion_proof(live_bridge.aleo_address(), program_id)
+ assert proof.count("leaf_index") == 2 and proof.count("field") == 32
+
+
+def test_balance_mappings_exist_for_every_aleo_asset(live_bridge):
+ for asset in live_bridge.registry.assets(chain="aleo"):
+ program = balance_program(asset)
+ if program is None:
+ continue
+ names = live_bridge.program(program).mappings()
+ assert BALANCE_MAPPING in names, f"{program} declares {sorted(names)}; adjust BALANCE_MAPPING/balance_program in client.py"
+
+
+def test_status_is_read_only_and_complete(live_bridge):
+ status = live_bridge.status()
+ assert status.chains[0].address == live_bridge.aleo_address()
+ assert set(status.chains[0].balances) == {a.id for a in live_bridge.registry.assets(chain="aleo")}
+ assert status.chains[0].balances["aleo/aleo"] > 0, \
+ f"expected the live-test funding key to hold a positive credits balance, got {status.chains[0].balances['aleo/aleo']}"
+
+
+def test_wrapper_program_is_deployed_with_expected_transitions(live_bridge):
+ assert enc.aleo_program_address("shielded_usdcx_wrapper.aleo", "mainnet") == \
+ "aleo183r3zgsr57fwtgk5duzeq9kqdkpmmtfj4k5469ddvm3tcfhhls9szktw82"
+ functions = live_bridge.program("shielded_usdcx_wrapper.aleo").functions
+ assert "private_mint" in functions and "private_burn" in functions
+ bridge_functions = live_bridge.program("usdcx_bridge_v2.aleo").functions
+ assert "burn_public" in bridge_functions and "burn_public_as_signer" in bridge_functions
+ for asset in ALEO_ORIGIN:
+ router = live_bridge.program(live_bridge.hyperlane.outbound_route(asset).metadata["aleoRouterProgram"]).functions
+ assert "transfer_remote" in router and "transfer_remote_as_signer" in router
+
+
+def test_transfer_remote_simulate(live_bridge):
+ if os.environ.get("BRIDGE_LIVE_SIMULATE") != "1":
+ pytest.skip("set BRIDGE_LIVE_SIMULATE=1 to build a local authorization (downloads proving parameters; no broadcast)")
+ call = live_bridge.hyperlane.transfer_remote("aleo/wbtc", "0x0000000000000000000000000000000000000001", amount_atomic=1, as_signer=True)
+ assert "amount: " in call.inputs[6] and call.inputs[5] == "1u128"
+ try:
+ authorization = call.simulate()
+ except Exception as exc: # noqa: BLE001 — the facade's own authorization error type varies by binding version
+ reason = str(exc)
+ if "balance" in reason.lower() or "insufficient" in reason.lower():
+ pytest.xfail(reason=f"authorization failed for lack of a wrapped-WBTC balance on the live-test key: {reason}")
+ raise
+ assert authorization.function_name == "transfer_remote_as_signer"
+ assert live_bridge.aleo.network.get_latest_height() > 0 # sanity: the same client reaches the node; nothing was submitted
diff --git a/bridge-sdk/tests/live/test_eth_reads.py b/bridge-sdk/tests/live/test_eth_reads.py
new file mode 100644
index 0000000..6afb772
--- /dev/null
+++ b/bridge-sdk/tests/live/test_eth_reads.py
@@ -0,0 +1,108 @@
+"""Read-only mainnet checks: quotes for all three Hyperlane routes, the xReserve deposit quote,
+Mailbox.delivered, and a pinned-address balance read.
+
+Gated by BRIDGE_LIVE_READS=1 and ETHEREUM_RPC_URL. Nothing here signs or needs a key — the
+``Ethereum`` connection is built with no ``private_key``/``signer``, and the ``Bridge`` is built
+over a real (keyless) ``aleo.Aleo`` facade so only the ``bridge.eth`` surface is exercised.
+
+A public RPC's rate limiting (HTTP 429) or a transient 5xx is not a bug in this SDK, so those are
+skips, not failures.
+"""
+import os
+
+import pytest
+import requests
+
+from aleo_bridge.errors import InsufficientBalanceError
+from aleo_bridge.eth import Ethereum
+
+pytestmark = [
+ pytest.mark.live,
+ pytest.mark.skipif(
+ os.environ.get("BRIDGE_LIVE_READS") != "1" or not os.environ.get("ETHEREUM_RPC_URL"),
+ reason="set BRIDGE_LIVE_READS=1 and ETHEREUM_RPC_URL"),
+]
+
+ALEO_RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+# The xReserve contract custodies deposited USDC, so its balance exceeds the 2 USDC minimum and its
+# allowance reads are meaningful. Any funded mainnet address works; keep it read-only.
+PINNED_SENDER = "0x8888888199b2Df864bf678259607d6D5EBb4e3Ce"
+KNOWN_MESSAGE_ID = "0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d" # delivered on Aleo
+
+
+def _run(fn):
+ """Run *fn*; a 429/5xx from the public RPC is an environment condition, not a test failure."""
+ try:
+ return fn()
+ except requests.exceptions.HTTPError as exc:
+ status = exc.response.status_code if exc.response is not None else None
+ if status == 429 or (status is not None and status >= 500):
+ pytest.skip(f"public RPC rate-limited (HTTP {status})")
+ raise
+ except requests.exceptions.ConnectionError as exc:
+ pytest.skip(f"public RPC unreachable: {exc}")
+ except requests.exceptions.Timeout:
+ pytest.skip("public RPC timed out")
+
+
+@pytest.fixture(scope="module")
+def eth():
+ from aleo import Aleo, HTTPProvider
+
+ from aleo_bridge import Bridge
+
+ aleo = Aleo(HTTPProvider("https://edge.provable.com/api", network="mainnet")) # no default_account: read-only
+ bridge = Bridge(aleo, ethereum=Ethereum(os.environ["ETHEREUM_RPC_URL"]))
+ return bridge.eth
+
+
+def test_connection_is_mainnet_and_read_only(eth):
+ chain_id = _run(lambda: eth.conn.chain_id)
+ assert chain_id == 1 and not eth.conn.can_sign and eth.chain.id == "ethereum"
+
+
+@pytest.mark.parametrize("asset,amount_atomic,router_type", [("eth", 1, "native"), ("wbtc", 1, "collateral"), ("usdt", 1, "collateral")])
+def test_hyperlane_quotes_at_minimum_amounts(eth, asset, amount_atomic, router_type, record_property):
+ q = _run(lambda: eth.quote_transfer_remote(asset, ALEO_RECIPIENT, amount_atomic=amount_atomic, sender=PINNED_SENDER))
+ record_property(f"hyperlane_quote_{asset}", {
+ "native_fee_atomic": q.native_fee_atomic, "native_value_atomic": q.native_value_atomic,
+ "approval_required": q.approval_required})
+ assert q.native_fee_atomic > 0 and q.plan.amount_atomic == amount_atomic
+ if router_type == "native":
+ assert q.native_value_atomic == amount_atomic + q.native_fee_atomic and q.approval_required is None
+ else:
+ assert q.native_value_atomic == q.native_fee_atomic and isinstance(q.approval_required, bool)
+ assert q.fees[0].asset_id == "ethereum/eth" and q.fees[0].estimated
+
+
+def test_xreserve_deposit_quote_read_only(eth, record_property):
+ try:
+ q = _run(lambda: eth.quote_deposit_usdc(ALEO_RECIPIENT, amount="2", sender=PINNED_SENDER))
+ except InsufficientBalanceError as exc:
+ # An accepted outcome (controller ruling): PINNED_SENDER's on-chain USDC balance may have
+ # moved since this test was written. The exception itself proves the balance-check mechanics
+ # ran (the SDK read PINNED_SENDER's USDC balance before building a quote).
+ record_property("xreserve_quote_outcome", f"InsufficientBalanceError: {exc}")
+ return
+ record_property("xreserve_quote_outcome", {
+ "route_id": q.plan.route_id, "max_fee_atomic": q.max_fee_atomic, "balance_atomic": q.balance_atomic})
+ assert q.plan.route_id == "xreserve:ethereum/usdc->aleo/usdcx" and q.max_fee_atomic == 100_000
+ assert q.hook_data == bytes(65) and len(q.remote_recipient_bytes32) == 32 and q.balance_atomic >= 2_000_000
+
+
+def test_mailbox_delivered_reads(eth, record_property):
+ delivered = _run(lambda: eth.is_delivered(KNOWN_MESSAGE_ID))
+ record_property("mailbox_delivered_known_id", delivered)
+ assert isinstance(delivered, bool)
+ assert _run(lambda: eth.is_delivered("0x" + "00" * 32)) is False
+
+
+def test_balance_pinned_sender(eth, record_property):
+ value = _run(lambda: eth.balance("ethereum/eth", address=PINNED_SENDER))
+ record_property("eth_balance_pinned_sender", value)
+ assert isinstance(value, int) and value >= 0
+
+
+def test_chain_status_read_only(eth):
+ status = _run(lambda: eth.chain_status())
+ assert status.chain_id == "ethereum" and status.address is None and status.balances == {}
diff --git a/bridge-sdk/tests/live/test_eth_sepolia_leg1.py b/bridge-sdk/tests/live/test_eth_sepolia_leg1.py
new file mode 100644
index 0000000..a118ab2
--- /dev/null
+++ b/bridge-sdk/tests/live/test_eth_sepolia_leg1.py
@@ -0,0 +1,98 @@
+"""Leg 1 of the acceptance matrix: 2 USDC Sepolia -> USDCx aleo-testnet, public mint.
+
+Moves testnet funds. Gated by BRIDGE_LIVE_FUNDS=1, BRIDGE_LIVE_STATE_DIR, SEPOLIA_RPC_URL,
+EVM_PRIVATE_KEY, ALEO_E2E_PRIVATE_KEY. Every checkpoint and the final receipt are written to
+BRIDGE_LIVE_STATE_DIR (outside the repo) so plan 4's rehearsal runner can `recover`/`wait` and
+run leg 2 from them. Keys are read from the environment and never written or printed.
+
+Output layout matches spec §12 / plan 4's rehearsal convention:
+ - /checkpoints/.json — the FileCheckpointStore (one file per receipt id)
+ - /leg1.json — latest checkpoint boundary, overwritten atomically
+ - /receipts.jsonl — one appended line per leg run
+"""
+import json
+import os
+import tempfile
+import time
+from pathlib import Path
+
+import pytest
+
+from aleo_bridge.checkpoint import FileCheckpointStore
+from aleo_bridge.eth import Ethereum
+from aleo_bridge.types import Status
+
+REQUIRED = ("BRIDGE_LIVE_STATE_DIR", "SEPOLIA_RPC_URL", "EVM_PRIVATE_KEY", "ALEO_E2E_PRIVATE_KEY")
+pytestmark = [
+ pytest.mark.live,
+ pytest.mark.skipif(
+ os.environ.get("BRIDGE_LIVE_FUNDS") != "1" or any(not os.environ.get(v) for v in REQUIRED),
+ reason="set BRIDGE_LIVE_FUNDS=1, BRIDGE_LIVE_STATE_DIR, SEPOLIA_RPC_URL, EVM_PRIVATE_KEY, ALEO_E2E_PRIVATE_KEY"),
+]
+
+
+def _atomic_write(path: Path, text: str) -> None:
+ """Write *text* to *path* via a same-directory temp file + os.replace (no partial reads)."""
+ fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".", suffix=".tmp")
+ try:
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
+ fh.write(text)
+ os.replace(tmp, path)
+ except BaseException:
+ try:
+ os.unlink(tmp)
+ except FileNotFoundError:
+ pass
+ raise
+
+
+def _bridge():
+ from aleo import Aleo, HTTPProvider
+
+ from aleo_bridge import Bridge
+
+ aleo = Aleo(HTTPProvider(os.environ.get("ALEO_ENDPOINT", "https://edge.provable.com/api"), network="testnet"))
+ aleo.default_account = aleo.account.from_private_key(os.environ["ALEO_E2E_PRIVATE_KEY"])
+ state_dir = Path(os.environ["BRIDGE_LIVE_STATE_DIR"])
+ state_dir.mkdir(parents=True, exist_ok=True)
+ return Bridge(aleo, ethereum=Ethereum(os.environ["SEPOLIA_RPC_URL"], private_key=os.environ["EVM_PRIVATE_KEY"]),
+ checkpoints=FileCheckpointStore(state_dir / "checkpoints")), state_dir
+
+
+def test_sepolia_usdc_deposit_public_mint():
+ bridge, state_dir = _bridge()
+ eth = bridge.eth
+ assert eth.conn.chain_id == 11155111 and eth.chain.id == "sepolia"
+ recipient = bridge.aleo_address()
+ usdc_balance = eth.balance("usdc")
+ if usdc_balance < 2_000_000:
+ pytest.fail(f"{eth.conn.address} holds {usdc_balance} µUSDC on Sepolia; leg 1 needs 2 USDC plus ETH for gas")
+ quote = eth.quote_deposit_usdc(recipient, amount="2", mint_mode="public")
+ assert quote.plan.route_id == "xreserve:sepolia/usdc->aleo-testnet/usdcx" and quote.plan.recipient == recipient
+
+ checkpoint_path = state_dir / "leg1.json"
+
+ def save(checkpoint):
+ _atomic_write(checkpoint_path, checkpoint.to_json() + "\n") # latest boundary wins; the store keeps every id
+
+ result = eth.deposit_usdc(recipient, amount="2", mint_mode="public").send(
+ on_checkpoint=save, timeout_seconds=240.0, poll_seconds=3.0)
+ receipt = result.receipt
+
+ with (state_dir / "receipts.jsonl").open("a", encoding="utf-8") as fh:
+ fh.write(json.dumps({
+ "leg": 1, "route_id": result.route_id, "source_tx": receipt.source_tx_id,
+ "message_hash": result.message_hash, "status": receipt.status.value,
+ "delivery_confirmed": False, "amount_atomic": quote.plan.amount_atomic,
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+ }) + "\n")
+
+ assert checkpoint_path.exists() and bridge.checkpoints.load(receipt.id) is not None
+ assert receipt.status in (Status.ATTESTATION_PENDING, Status.SOURCE_CONFIRMING, Status.SOURCE_APPROVAL_PENDING)
+ if receipt.status == Status.ATTESTATION_PENDING:
+ assert len(result.message_hash) == 66 and len(result.nonce) == 66
+ assert receipt.protocol_state["depositLogIndex"] >= 0 and receipt.protocol_state["mintMode"] == "public"
+ else:
+ # A timeout is not a failure: the receipt and checkpoint carry the hashes for recover()/source_status().
+ refreshed = eth.source_status(quote.plan, receipt)
+ assert refreshed.status != Status.FAILED
diff --git a/bridge-sdk/tests/live/test_lifecycle_live.py b/bridge-sdk/tests/live/test_lifecycle_live.py
new file mode 100644
index 0000000..663187f
--- /dev/null
+++ b/bridge-sdk/tests/live/test_lifecycle_live.py
@@ -0,0 +1,382 @@
+"""The funded live suite: every registry route, back and forth, through the public lifecycle verbs.
+
+One test per veil case, parametrized over every route that case covers. The parameter set is built
+by ENUMERATING the registry, not by listing route ids by hand, so a new route cannot be added
+without a case: :func:`_by_case` raises at import when a route has no case, and a route whose
+``availability`` is not ``active`` is parametrized and skipped with ``registry:``
+rather than dropped. Plus the testnet deposit (`xreserve:sepolia/usdc->aleo-testnet/usdcx`) and,
+beyond veil, the testnet RETURN leg (`xreserve:aleo-testnet/usdcx->sepolia/usdc`).
+
+Gates (read only — nothing here sets, exports or prints a settable acknowledgement):
+
+* ``BRIDGE_LIVE_FUNDS=1`` + ``BRIDGE_LIVE_STATE_DIR`` — the funded tests exist at all.
+* ``BRIDGE_LIVE_MAINNET_ACK`` + ``BRIDGE_LIVE_MAINNET_CASES`` — a named mainnet case may run.
+* ``BRIDGE_LIVE_MAINNET_EXECUTE`` — the wallet may submit. Without it every mainnet case runs to
+ the quote and returns (veil's ``if (!mainnetExecutionEnabled()) return``), which is how the
+ quote-only sweep in the report was produced.
+
+Every funded test goes through the disk: phase one quotes, prechecks and executes, then RETURNS —
+the client is thrown away; phase two builds a brand new :class:`Bridge` over the same
+``FileCheckpointStore`` and finishes the transfer from ``bridge.pending()`` / ``bridge.recover()``.
+``execute`` is called at most once per transfer, ever; a timeout is *pending* (skip + the resume
+command), never a failure verdict, and ``Underfunded`` is a skip printing the shortfall.
+"""
+from __future__ import annotations
+
+import time
+from pathlib import Path
+from typing import Any, Callable
+
+import pytest
+
+from aleo_bridge.errors import InsufficientBalanceError, PollingTimeoutError
+from aleo_bridge.registry import DEFAULT_REGISTRY, Route
+
+from . import cases as live_cases
+from . import config as live_config
+from .helpers import LiveBenchmark, LiveTimeoutError, Underfunded
+
+pytestmark = pytest.mark.live
+
+#: The testnet pair. The deposit amount is 3 USDC (controller ruling 2026-09-18): the minted 3
+#: USDCx clears the 2 USDCx withdrawal fee so the return leg can burn veil's 2.000001 afterwards.
+TESTNET_DEPOSIT_ROUTE = "xreserve:sepolia/usdc->aleo-testnet/usdcx"
+TESTNET_RETURN_ROUTE = "xreserve:aleo-testnet/usdcx->sepolia/usdc"
+TESTNET_DEPOSIT_AMOUNT = "3"
+TESTNET_RETURN_AMOUNT = "2.000001" # veil mainnet/aleo-xreserve.live.test.ts:97
+
+
+# ── parametrization: enumerate the registry, leave nothing out ───────────────
+
+def _by_case(environment: str) -> dict[str, list[Route]]:
+ """``case → routes`` for *environment*, asserting that every route has a case.
+
+ Hermetic: the registry is static data, so this runs at import with no network and no keys.
+ """
+ buckets: dict[str, list[Route]] = {name: [] for name in live_cases.CASE_NAMES}
+ uncovered: list[str] = []
+ for route in DEFAULT_REGISTRY.routes(environment=environment):
+ name = live_cases.case_for_route(DEFAULT_REGISTRY, route)
+ if name is None:
+ uncovered.append(route.id)
+ else:
+ buckets[name].append(route)
+ if uncovered:
+ raise AssertionError(
+ f"No live case covers these {environment} routes: {sorted(uncovered)} — add the case "
+ "to tests/live/cases.py::CASES rather than narrowing this suite")
+ return buckets
+
+
+MAINNET_ROUTES = _by_case("mainnet")
+TESTNET_ROUTES = _by_case("testnet")
+
+
+def _ids(routes: list[Route]) -> list[str]:
+ return [route.id for route in routes]
+
+
+def _params(case: str) -> Any:
+ """``pytest.mark.parametrize`` arguments for one case: every mainnet route it covers."""
+ routes = MAINNET_ROUTES[case]
+ return pytest.mark.parametrize("route", routes, ids=_ids(routes))
+
+
+# ── clients ──────────────────────────────────────────────────────────────────
+
+def _build_bridge(environment: str) -> Any:
+ """A fresh client for *environment* — new facade, new connections, same checkpoint store.
+
+ Keys and endpoints are resolved only through ``config``: the testnet bridge can never pick up
+ the mainnet Aleo key, and an unset RPC variable falls back to the public default.
+ """
+ from aleo_bridge import Bridge
+ from aleo_bridge.checkpoint import FileCheckpointStore
+ from aleo_bridge.client import build_aleo
+ from aleo_bridge.eth import Ethereum
+ from aleo_bridge.sol import Solana
+
+ aleo = build_aleo(live_config.aleo_endpoint(), live_config.ALEO_NETWORKS[environment],
+ live_config.aleo_private_key(environment))
+ ethereum = Ethereum(live_config.evm_rpc_url(environment),
+ private_key=live_config.evm_private_key(environment))
+ solana = Solana.from_env() if environment == "mainnet" else None
+ store = FileCheckpointStore(live_config.state_dir() / environment / "checkpoints")
+ bridge = Bridge(aleo, ethereum=ethereum, solana=solana, checkpoints=store)
+ assert bridge.environment == environment
+ return bridge
+
+
+#: Opt-in for sharing a MAINNET view key with the hosted record scanner (see below).
+SCANNER_VAR = "BRIDGE_LIVE_ALEO_SCANNER"
+
+
+def _register_record_scanner(bridge: Any, environment: str) -> None:
+ """Register the Aleo account with the hosted record scanner, which private selection needs.
+
+ ``bridge.privacy.select_record`` (the record the xReserve burn spends) goes through
+ ``aleo.records.find``, and the scanner answers nothing for an account it has not been
+ registered for — it raises ``UUIDError`` instead. Registration **shares that account's view
+ key** with the scanning service, which can then decrypt every record the account owns.
+
+ That is a decision about somebody's privacy, so it is automatic only for the testnet e2e key.
+ On mainnet the case skips unless ``BRIDGE_LIVE_ALEO_SCANNER=1`` says the operator accepts it;
+ nothing here ever sets that variable.
+ """
+ if environment != "testnet" and live_config.value(SCANNER_VAR) != "1":
+ pytest.skip(f"private-record selection needs the hosted record scanner, and registering "
+ f"shares this account's view key with it — set {SCANNER_VAR}=1 to accept that")
+ result = bridge.aleo.records.register(bridge.aleo.default_account)
+ ok = result.get("ok") if isinstance(result, dict) else result
+ print(f" scanner hosted record scanner registered for {bridge.aleo_address()} (ok={ok})")
+
+
+def _client(make_bridge: Callable[[], Any], case: str, environment: str, *, execute: bool) -> Any:
+ """A fresh bridge, with the record scanner registered for the cases that select private records.
+
+ Only when we are actually going to burn: a quote-only rehearsal never selects a record, so it
+ must not make a view-key decision (nor skip for want of one) on the operator's behalf.
+ """
+ bridge = make_bridge()
+ if execute and case == "aleo-xreserve":
+ _register_record_scanner(bridge, environment)
+ return bridge
+
+
+@pytest.fixture
+def mainnet_client() -> Callable[[], Any]:
+ return lambda: _build_bridge("mainnet")
+
+
+@pytest.fixture
+def testnet_client() -> Callable[[], Any]:
+ return lambda: _build_bridge("testnet")
+
+
+# ── the shared body ──────────────────────────────────────────────────────────
+
+def _state_path(case: str, route: Route, environment: str) -> Path:
+ return live_config.live_state_path(environment, live_cases.state_name(case, route.id))
+
+
+def resume_command(state_path: Path | str) -> str:
+ """What the operator types to continue an interrupted transfer (the CLI, not a test run)."""
+ return f"python scripts/rehearse.py --recover {state_path}"
+
+
+def _destination_family(route: Route) -> str:
+ asset = DEFAULT_REGISTRY.asset(route.destination_asset_id)
+ return DEFAULT_REGISTRY.chain(asset.chain_id).family
+
+
+def _record(record_property: Callable[[str, Any], None], case: str, route: Route, state: Any,
+ benchmark: LiveBenchmark) -> None:
+ record_property("case", case)
+ record_property("route_id", route.id)
+ record_property("source_tx_id", getattr(state, "source_tx_id", None))
+ record_property("message_id", getattr(state, "message_id", None))
+ record_property("destination_tx_id", getattr(state, "destination_tx_id", None))
+ record_property("destination_balance_before", getattr(state, "destination_balance_before", None))
+ record_property("completed", bool(getattr(state, "completed", False)))
+ record_property("benchmark_ms", benchmark.as_dict())
+
+
+def _drive_case(make_bridge: Callable[[], Any], case: str, route: Route, *, environment: str,
+ execute: bool, record_property: Callable[[str, Any], None],
+ amount: str | None = None) -> Any:
+ """Quote (and, when acknowledged, execute) one route, then finish it from disk with a NEW client.
+
+ The two phases are the point: whatever ``execute`` returned is dropped with the first client, so
+ the transfer can only reach ``done`` through ``bridge.pending()`` → ``bridge.recover()`` →
+ ``wait``/``resume``/``complete`` on a process that never saw it start.
+ """
+ if not route.active:
+ pytest.skip(f"registry:{route.availability}")
+
+ state_path = _state_path(case, route, environment)
+ benchmark = LiveBenchmark(f"{case}:{live_cases.route_slug(route.id)}", log=print)
+ amount = amount or live_config.case_amount_override(case)
+ recipient = live_config.recipient_override(_destination_family(route))
+ deadline = time.monotonic() + live_cases.CASE_TIMEOUT_SECONDS
+
+ def remaining() -> float:
+ return max(deadline - time.monotonic(), 0.0)
+
+ def run(bridge: Any, *, stop_after_execute: bool) -> Any:
+ return live_cases.run_case(
+ bridge, case, route.id, state_path=state_path, recipient=recipient, amount=amount,
+ execute=execute, benchmark=benchmark, stop_after_execute=stop_after_execute,
+ wait_timeout_seconds=min(live_cases.WAIT_TIMEOUT_SECONDS, remaining()),
+ wait_poll_seconds=live_cases.WAIT_POLL_SECONDS, log=print)
+
+ state = None
+ try:
+ bridge = _client(make_bridge, case, environment, execute=execute)
+ benchmark.mark("clients-created")
+ state = run(bridge, stop_after_execute=True)
+ if execute and state.checkpoint is not None and not state.completed:
+ print(f" handover dropping the client that executed {route.id}; "
+ "a new one will recover from disk")
+ del bridge # the in-memory progress goes with it
+ bridge = _client(make_bridge, case, environment, execute=execute)
+ benchmark.mark("clients-recreated")
+ state = run(bridge, stop_after_execute=False)
+ except Underfunded as exc:
+ record_property("underfunded", {"asset_id": exc.asset_id, "needed": exc.needed,
+ "have": exc.have, "shortfall": exc.shortfall, "what": exc.what})
+ print(f"\n UNDERFUNDED {route.id}: {exc}")
+ pytest.skip(f"{route.id}: {exc}")
+ except InsufficientBalanceError as exc:
+ # Some quotes read the wallet themselves and refuse before our precheck ever runs (the
+ # xReserve deposit quote is one). That is the same verdict, reached one step earlier.
+ record_property("underfunded", {"raised_by": "quote", "detail": str(exc)})
+ print(f"\n UNDERFUNDED {route.id}: the quote refused — {exc}")
+ pytest.skip(f"{route.id}: {exc}")
+ except (PollingTimeoutError, LiveTimeoutError) as exc:
+ record_property("pending_state_path", str(state_path))
+ record_property("pending_resume", resume_command(state_path))
+ print(f"\n PENDING {route.id} is still in flight: {exc}\n"
+ f" resume {resume_command(state_path)}")
+ pytest.skip(f"{route.id} still in flight (state {state_path}); resume: {resume_command(state_path)}")
+
+ _record(record_property, case, route, state, benchmark)
+ if not execute:
+ # veil's `if (!mainnetExecutionEnabled()) return` — the quote and the precheck are the test,
+ # and the one thing that must hold is that nothing was submitted for an unstarted transfer.
+ assert state.completed or state.checkpoint is None, \
+ f"{route.id} has an in-flight checkpoint that the quote-only run must not have created"
+ return state
+
+ assert state.completed, f"{route.id} did not reach done: {state}"
+ assert state.source_tx_id, f"{route.id} completed without a source transaction id"
+ if not live_cases.delivery_is_a_balance_rise(route, DEFAULT_REGISTRY):
+ assert state.message_id or state.destination_tx_id, \
+ f"{route.id} completed without a message id or a destination transaction id"
+
+ # Delivery is checked as "at least what was quoted", never as equality. A quote's `amount_out`
+ # is derived from the registry's fee literal, and the fee the protocol actually charges is live
+ # state: the 2026-09-18 testnet return burned 2.000001 USDCx against a registry
+ # withdrawalFeeAtomic of 2_000_000 (quote: 0.000001 USDC out) and delivered 0.996501 USDC,
+ # because Circle's testnet withdrawal fee was 1.0035 USDC that day. A private mint moves no
+ # public balance at all, so a zero delta is also correct.
+ if state.destination_balance_before is not None:
+ after = live_cases.read_balances(bridge).get(route.destination_asset_id)
+ if after is not None:
+ delta = after - int(state.destination_balance_before)
+ record_property("destination_balance_delta", delta)
+ print(f" delta {route.destination_asset_id} +{delta} atomic "
+ f"(before {state.destination_balance_before}, after {after})")
+ assert delta >= 0, f"{route.id} delivered a negative balance delta ({delta})"
+ if live_cases.delivery_is_a_balance_rise(route, DEFAULT_REGISTRY):
+ assert delta > 0, f"{route.id} was marked delivered but nothing arrived"
+ print(f"\n SUMMARY {case} {route.id} source={state.source_tx_id} "
+ f"message={state.message_id} destination={state.destination_tx_id}")
+ return state
+
+
+def _mainnet(case: str, route: Route, make_bridge: Callable[[], Any],
+ record_property: Callable[[str, Any], None]) -> Any:
+ """Gate one mainnet route: funds + the case acknowledgement; submission needs its own."""
+ if not live_config.live_funds_enabled():
+ pytest.skip(f"set {live_config.FUNDS_VAR}=1 and {live_config.STATE_DIR_VAR} to run funded live cases")
+ if not live_config.mainnet_case_enabled(case):
+ pytest.skip(f"{live_config.MAINNET_ACK_VAR} and {live_config.MAINNET_CASES_VAR} do not enable {case}")
+ return _drive_case(make_bridge, case, route, environment="mainnet",
+ execute=live_config.mainnet_execution_enabled(), record_property=record_property)
+
+
+def _testnet(case: str, route_id: str, make_bridge: Callable[[], Any],
+ record_property: Callable[[str, Any], None], amount: str) -> Any:
+ """Gate one testnet route: the funds gate alone (veil's testnet file has no mainnet acknowledgement)."""
+ if not live_config.live_funds_enabled():
+ pytest.skip(f"set {live_config.FUNDS_VAR}=1 and {live_config.STATE_DIR_VAR} to run funded live cases")
+ route = DEFAULT_REGISTRY.route(route_id)
+ return _drive_case(make_bridge, case, route, environment="testnet", execute=True,
+ record_property=record_property, amount=amount)
+
+
+# ── the five mainnet cases, one test each, over every route they cover ───────
+
+@_params("evm-hyperlane")
+def test_evm_hyperlane(route, mainnet_client, record_property):
+ """veil mainnet/evm-hyperlane.live.test.ts: ethereum → aleo over the Hyperlane warp routes."""
+ _mainnet("evm-hyperlane", route, mainnet_client, record_property)
+
+
+@_params("evm-xreserve")
+def test_evm_xreserve(route, mainnet_client, record_property):
+ """veil mainnet/evm-xreserve.live.test.ts: 2 USDC ethereum → aleo, private mint (needs `complete`)."""
+ _mainnet("evm-xreserve", route, mainnet_client, record_property)
+
+
+@_params("aleo-hyperlane")
+def test_aleo_hyperlane(route, mainnet_client, record_property):
+ """veil mainnet/aleo-hyperlane.live.test.ts: aleo → ethereum and aleo → solana, `mode="signer"`."""
+ _mainnet("aleo-hyperlane", route, mainnet_client, record_property)
+
+
+@_params("aleo-xreserve")
+def test_aleo_xreserve(route, mainnet_client, record_property):
+ """veil mainnet/aleo-xreserve.live.test.ts: 2.000001 USDCx private burn, aleo → ethereum."""
+ _mainnet("aleo-xreserve", route, mainnet_client, record_property)
+
+
+@_params("solana-hyperlane")
+def test_solana_hyperlane(route, mainnet_client, record_property):
+ """veil mainnet/solana-hyperlane.live.test.ts: 1 lamport solana → aleo."""
+ _mainnet("solana-hyperlane", route, mainnet_client, record_property)
+
+
+# ── recovery for real (controller note 10, brief deliverable 2) ──────────────
+
+@pytest.mark.parametrize("route", MAINNET_ROUTES["evm-hyperlane"][:1],
+ ids=_ids(MAINNET_ROUTES["evm-hyperlane"][:1]))
+def test_evm_hyperlane_recovers_from_disk_after_execute(route, mainnet_client, record_property):
+ """The ETH route, finished by a client that never saw ``execute``.
+
+ This is the same two-phase body every funded test uses, named separately because the parity doc
+ calls it out: the checkpoint written by phase one is looked up through ``bridge.pending()`` on a
+ brand-new ``Bridge`` bound to the same ``FileCheckpointStore``, rebuilt with ``bridge.recover``
+ and driven to ``done`` with ``wait``.
+ """
+ state = _mainnet("evm-hyperlane", route, mainnet_client, record_property)
+ if live_config.mainnet_execution_enabled():
+ assert state.checkpoint is not None, "the transfer must have left a checkpoint on disk"
+
+
+# ── testnet: the pair that actually runs today ───────────────────────────────
+
+def test_testnet_evm_xreserve_deposit(testnet_client, record_property):
+ """veil testnet/evm-xreserve.live.test.ts: 3 USDC Sepolia → aleo-testnet USDCx, private mint.
+
+ 3 rather than veil's 2 (controller ruling 2026-09-18) so the minted balance clears the 2 USDCx
+ withdrawal fee and :func:`test_testnet_aleo_xreserve_return` can burn straight afterwards.
+ """
+ _testnet("evm-xreserve", TESTNET_DEPOSIT_ROUTE, testnet_client, record_property,
+ TESTNET_DEPOSIT_AMOUNT)
+
+
+def test_testnet_aleo_xreserve_return(testnet_client, record_property):
+ """Beyond veil: the RETURN leg, aleo-testnet USDCx → Sepolia USDC, 2.000001 private burn.
+
+ Runs the mainnet ``aleo-xreserve`` case function against the testnet route, so "all the routes
+ back and forth" holds on testnet too. It spends what the deposit minted — run that first; an
+ unfunded run skips with the shortfall.
+ """
+ _testnet("aleo-xreserve", TESTNET_RETURN_ROUTE, testnet_client, record_property,
+ TESTNET_RETURN_AMOUNT)
+
+
+# ── the coverage invariant, as a test as well as an import-time assertion ────
+
+def test_every_registry_route_has_a_case():
+ """'All the routes back and forth': no route in either environment may be left without a case."""
+ for environment in live_config.ENVIRONMENTS:
+ buckets = _by_case(environment)
+ listed = {route.id for routes in buckets.values() for route in routes}
+ assert listed == {route.id for route in DEFAULT_REGISTRY.routes(environment=environment)}
+ assert {route.id for route in TESTNET_ROUTES["evm-xreserve"]} == {TESTNET_DEPOSIT_ROUTE}
+ assert {route.id for route in TESTNET_ROUTES["aleo-xreserve"]} == {TESTNET_RETURN_ROUTE}
+
+
+__all__ = ["MAINNET_ROUTES", "TESTNET_ROUTES", "resume_command"]
diff --git a/bridge-sdk/tests/live/test_sol_reads.py b/bridge-sdk/tests/live/test_sol_reads.py
new file mode 100644
index 0000000..38107bd
--- /dev/null
+++ b/bridge-sdk/tests/live/test_sol_reads.py
@@ -0,0 +1,82 @@
+"""Read-only mainnet checks for the Solana side. Gate: BRIDGE_LIVE_READS=1.
+
+No key, no funds: decodes the live inner IGP account and quotes leg 11 (SOL -> Aleo, 1 lamport) for a
+pinned sender. Fund-moving legs 11-12 run from scripts/rehearse.py (plan 4).
+
+A public RPC's rate limiting (HTTP 429) or a transient 5xx is not a bug in this SDK, so those are
+skips, not failures (mirrors tests/live/test_eth_reads.py's ``_run`` helper).
+"""
+import os
+import re
+
+import pytest
+
+pytest.importorskip("solders")
+
+from aleo import Aleo, HTTPProvider
+
+from aleo_bridge import Bridge, Solana
+from aleo_bridge import _sealevel as sl
+from aleo_bridge.errors import BridgeError
+from tests.fakes.sealevel_fixtures import ALEO_MAINNET_DOMAIN, DESTINATION_GAS_AMOUNT, TRANSFER, igp_account_data
+
+pytestmark = [
+ pytest.mark.live,
+ pytest.mark.skipif(os.environ.get("BRIDGE_LIVE_READS") != "1", reason="set BRIDGE_LIVE_READS=1 to hit mainnet RPCs"),
+]
+
+ALEO_ENDPOINT = os.environ.get("ALEO_ENDPOINT", "https://edge.provable.com/api")
+SENDER = os.environ.get("BRIDGE_LIVE_SOLANA_SENDER", TRANSFER["senderAddress"])
+RECIPIENT = os.environ.get("BRIDGE_LIVE_ALEO_RECIPIENT", TRANSFER["recipientAleoAddress"])
+
+_HTTP_STATUS_RE = re.compile(r"HTTP status (\d+)")
+
+
+def _run(fn):
+ """Run *fn*; a 429/5xx (or an unreachable public RPC) is an environment condition, not a test failure."""
+ try:
+ return fn()
+ except BridgeError as exc:
+ message = str(exc)
+ match = _HTTP_STATUS_RE.search(message)
+ if match and (int(match.group(1)) == 429 or int(match.group(1)) >= 500):
+ pytest.skip(f"public RPC rate-limited or unavailable ({message})")
+ if "request failed:" in message:
+ pytest.skip(f"public RPC unreachable: {message}")
+ raise
+
+
+@pytest.fixture(scope="module")
+def bridge() -> Bridge:
+ aleo = Aleo(HTTPProvider(ALEO_ENDPOINT, network="mainnet"))
+ return Bridge(aleo, solana=Solana(os.environ.get("SOLANA_RPC_URL")))
+
+
+def test_live_igp_account_decodes_with_the_fixture_shape(bridge):
+ metadata = _run(lambda: bridge.sol.metadata())
+ live = _run(lambda: bridge.sol._account_data(metadata.igp_account))
+ assert live is not None
+ account = sl.decode_igp_account(live)
+ recorded = sl.decode_igp_account(igp_account_data())
+ assert account.bump == recorded.bump and account.beneficiary == recorded.beneficiary
+ assert account.owner == recorded.owner
+ oracle = account.gas_oracles[ALEO_MAINNET_DOMAIN]
+ assert oracle.token_decimals == recorded.gas_oracles[ALEO_MAINNET_DOMAIN].token_decimals == 6
+ assert oracle.token_exchange_rate > 0 and oracle.gas_price > 0
+ lamports = sl.quote_igp_lamports(live, ALEO_MAINNET_DOMAIN, DESTINATION_GAS_AMOUNT)
+ assert 0 < lamports < 1_000_000_000 # sanity: below 1 SOL; the recorded value was 2_900_000
+
+
+def test_live_leg_11_quote_for_a_pinned_sender(bridge, record_property):
+ quote = _run(lambda: bridge.sol.quote_transfer_remote(RECIPIENT, amount_atomic=1, sender=SENDER))
+ assert quote.plan.route_id == sl.SOLANA_ROUTE_ID and quote.plan.sender == SENDER
+ assert quote.igp_lamports > 0 and quote.network_fee_lamports > 0
+ client = bridge.solana.client
+ rents = [int(_run(lambda n=n: client.get_minimum_balance_for_rent_exemption(n)).value) for n in (141, 194, 0)]
+ assert quote.rent_lamports == sum(rents)
+ assert quote.total_lamports == 1 + quote.igp_lamports + quote.network_fee_lamports + quote.rent_lamports
+ record_property("leg_11_quote", {
+ "igp_lamports": quote.igp_lamports, "network_fee_lamports": quote.network_fee_lamports,
+ "rent_lamports": quote.rent_lamports, "total_lamports": quote.total_lamports})
+ print(f"leg 11 quote: igp={quote.igp_lamports} fee={quote.network_fee_lamports} "
+ f"rent={quote.rent_lamports} total={quote.total_lamports}")
diff --git a/bridge-sdk/tests/test_agent.py b/bridge-sdk/tests/test_agent.py
new file mode 100644
index 0000000..163a0f1
--- /dev/null
+++ b/bridge-sdk/tests/test_agent.py
@@ -0,0 +1,436 @@
+"""Agent tools: the Claude-shape surface, the confirm gate, and what must never leave the process.
+
+The brief's own cases (surface/schemas, _serialize, reads, the four write gates) are kept as
+written; the controller's rulings add: no "0scalar" fallback for a private mint, no secret in any
+tool result, the synthetic xReserve max-fee entry, structured errors for an unconfigured chain,
+and `next: "recover"` guidance when a send's outcome is ambiguous.
+"""
+import json
+
+import pytest
+
+from aleo_bridge.agent import _serialize, _summarize_input, bridge_tools, dispatch_tool
+from aleo_bridge.checkpoint import Checkpoint, FileCheckpointStore, create_checkpoint
+from aleo_bridge.errors import BridgeError
+from aleo_bridge.lifecycle import prepare
+from aleo_bridge.types import Fee, Receipt, Status
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, RECORD_PLAINTEXT, FakeBridge
+
+READS = {"bridge_status", "bridge_list_assets", "bridge_list_routes", "bridge_quote", "bridge_get_progress",
+ "bridge_pending"}
+WRITES = {"bridge_execute", "bridge_resume", "bridge_complete", "bridge_shield", "bridge_unshield"}
+QUOTE_ARGS = {"source": "ethereum/usdc", "destination": "aleo/usdcx", "amount": "2", "recipient": ALEO_RECIPIENT}
+NONCE = "7scalar"
+
+
+def _aleo_out_checkpoint(b):
+ """A checkpoint for an Aleo-origin Hyperlane transfer that was proved but never broadcast."""
+ plan = prepare(b.registry, source="aleo/eth", destination="ethereum/eth", amount="0.000000000000000001",
+ recipient=EVM_ADDRESS)
+ serialized = json.dumps({"type": "execute", "id": "at1prepared", "fee": {}})
+ cp = {"version": 1,
+ "intent": {"source": {"chain": "aleo", "asset": "eth"},
+ "destination": {"chain": "ethereum", "asset": "eth"},
+ "bridgeProtocol": "hyperlane", "amount": plan.amount, "recipient": EVM_ADDRESS},
+ "route": {"id": plan.route_id, "registryVersion": plan.registry_version},
+ "source": {"preparedTransaction": {"transactionId": "at1prepared",
+ "serializedTransaction": serialized}}}
+ return cp, serialized
+
+
+def _inbound_private_checkpoint(b):
+ """A testnet xReserve deposit attested and waiting for its private mint."""
+ from aleo_bridge.types import Attestation
+ from tests.test_get_status import SIG, _inbound_private
+
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ b.xreserve.attestations[message_hash] = Attestation(payload, bytes.fromhex(message_hash[2:]),
+ bytes.fromhex(SIG[2:]), "complete")
+ b.eth.recover_result = receipt
+ return {"version": 1,
+ "intent": {"source": {"chain": "sepolia", "asset": "usdc"},
+ "destination": {"chain": "aleo-testnet", "asset": "usdcx"},
+ "bridgeProtocol": "xreserve", "amount": "2", "recipient": ALEO_RECIPIENT,
+ "mintMode": "private"},
+ "route": {"id": plan.route_id, "registryVersion": plan.registry_version},
+ "source": {"transactionId": "0x" + "22" * 32}}
+
+
+# ── surface ───────────────────────────────────────────────────────────────────
+
+def test_tool_surface_and_schemas():
+ tools = bridge_tools()
+ assert {t["name"] for t in tools} == READS | WRITES
+ assert {t["name"] for t in bridge_tools(include_writes=False)} == READS
+ for t in tools:
+ assert t["description"] and t["input_schema"]["type"] == "object"
+ json.dumps(t)
+ if t["name"] in WRITES:
+ assert t["input_schema"]["properties"]["confirm"]["type"] == "boolean"
+ assert "confirm" in t["description"].lower()
+ execute = next(t for t in tools if t["name"] == "bridge_execute")
+ assert set(execute["input_schema"]["required"]) == {"source", "destination", "amount", "recipient"}
+ assert "plan" not in execute["input_schema"]["properties"] # agents never carry plans
+ quote = next(t for t in tools if t["name"] == "bridge_quote")
+ assert quote["input_schema"]["properties"]["mint_mode"]["enum"] == ["public", "record", "private"]
+ assert "private key" not in json.dumps(tools).lower()
+
+
+def test_private_mint_descriptions_state_the_nonce_requirement():
+ tools = {t["name"]: t for t in bridge_tools()}
+ for name in ("bridge_quote", "bridge_execute", "bridge_complete"):
+ assert "secret_nonce" in tools[name]["input_schema"]["properties"]
+ for name in ("bridge_execute", "bridge_complete"):
+ description = tools[name]["input_schema"]["properties"]["secret_nonce"]["description"].lower()
+ assert "required" in description and "0scalar" not in description
+
+
+# ── _serialize ────────────────────────────────────────────────────────────────
+
+def test_serialize():
+ fee = Fee(kind="protocol", chain_id="aleo", asset_id="aleo/aleo", amount="8.174147", estimated=True)
+ out = _serialize({"fee": fee, "raw": b"\x01\xff", "status": Status.COMPLETED, "n": 10**20, "t": (1, 2), "none": None})
+ assert out == {"fee": {"kind": "protocol", "chain_id": "aleo", "asset_id": "aleo/aleo", "amount": "8.174147",
+ "estimated": True}, "raw": "0x01ff", "status": "COMPLETED", "n": 10**20, "t": [1, 2], "none": None}
+ json.dumps(out)
+
+
+def test_serialize_adds_the_xreserve_max_fee_entry():
+ b = FakeBridge()
+ quote = dispatch_tool(b, "bridge_quote", QUOTE_ARGS)
+ assert quote["fees"] == [{"kind": "protocol", "chain_id": "ethereum", "asset_id": "ethereum/usdc",
+ "amount": "0.1", "estimated": True, "label": "xReserve max fee"}]
+ # and the same entry appears when _serialize is handed the quote object on its own
+ from aleo_bridge import lifecycle
+ raw = lifecycle.quote(b, source="ethereum/usdc", destination="aleo/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT)
+ assert _serialize(raw)["fees"][-1]["label"] == "xReserve max fee"
+
+
+def test_serialize_drops_secret_and_bulky_receipt_state():
+ receipt = Receipt(id="0x01", protocol="xreserve", status=Status.ATTESTATION_PENDING,
+ protocol_state={"routeId": "r", "payload": "0x" + "ee" * 305, "attestation": "0x11",
+ "secretNonce": NONCE, "preparedTransaction": "{...}", "mintMode": "private"})
+ out = _serialize(receipt)
+ assert out["protocol_state"] == {"routeId": "r", "mintMode": "private"}
+ assert NONCE not in json.dumps(out)
+
+
+# ── reads ─────────────────────────────────────────────────────────────────────
+
+def test_read_tools():
+ b = FakeBridge()
+ b.public_balances["aleo/usdcx"] = 5_000_000
+ status = dispatch_tool(b, "bridge_status", {})
+ assert status["environment"] == "mainnet" and status["chains"][0]["balances"] == {"aleo/usdcx": 5_000_000}
+ assets = dispatch_tool(b, "bridge_list_assets", {"chain": "aleo"})
+ assert {a["symbol"] for a in assets} >= {"USDCx", "ETH", "WBTC", "USDT", "SOL"} and all(a["chain_id"] == "aleo" for a in assets)
+ routes = dispatch_tool(b, "bridge_list_routes", {"protocol": "xreserve"})
+ assert routes and all(r["protocol"] == "xreserve" and r["availability"] == "active" for r in routes)
+ all_routes = dispatch_tool(b, "bridge_list_routes", {"include_unavailable": True})
+ assert any(r["availability"] == "metadata-required" for r in all_routes)
+ q = dispatch_tool(b, "bridge_quote", {**QUOTE_ARGS, "mint_mode": "private", "secret_nonce": NONCE})
+ assert q["kind"] == "evm-xreserve" and q["plan"]["route_id"] == "xreserve:ethereum/usdc->aleo/usdcx"
+ assert q["plan"]["amount"] == "2" and q["hook_data"].startswith("0x")
+ json.dumps(q)
+ assert NONCE not in json.dumps(q)
+ assert b.events == [] # nothing signed
+
+
+# ── writes ────────────────────────────────────────────────────────────────────
+
+def test_execute_requires_confirm_and_requotes_internally():
+ b = FakeBridge()
+ out = dispatch_tool(b, "bridge_execute", QUOTE_ARGS)
+ assert out["confirmation_required"] is True and out["how_to_confirm"] == "re-call with confirm=true"
+ assert out["quote"]["kind"] == "evm-xreserve" and out["quote"]["plan"]["amount"] == "2"
+ assert [c[0] for c in b.calls] == ["eth.quote_deposit_usdc"] and b.events == []
+ b.calls.clear()
+ out = dispatch_tool(b, "bridge_execute", {**QUOTE_ARGS, "confirm": True})
+ assert [c[0] for c in b.calls] == ["eth.quote_deposit_usdc", "eth.deposit_usdc"]
+ assert out["progress"]["next"] == "wait" and out["progress"]["receipt"]["status"] == "ATTESTATION_PENDING"
+ assert out["checkpoint"]["version"] == 1 and out["checkpoint"]["route"]["id"] == "xreserve:ethereum/usdc->aleo/usdcx"
+ assert "secretNonce" not in json.dumps(out) and "payload" not in json.dumps(out["checkpoint"])
+ json.dumps(out)
+
+
+def test_get_progress_and_pending(tmp_path):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ out = dispatch_tool(b, "bridge_execute", {"source": "aleo/eth", "destination": "ethereum/eth",
+ "amount": "0.000000000000000001", "recipient": EVM_ADDRESS,
+ "gas_payment_microcredits": 1, "confirm": True})
+ assert out["progress"]["receipt"]["status"] == "SOURCE_CONFIRMING"
+ recovered = dispatch_tool(b, "bridge_get_progress", {"checkpoint": out["checkpoint"]})
+ assert recovered["progress"]["next"] == "wait"
+ assert recovered["progress"]["receipt"]["source_tx_id"] == "at1fake1"
+ # a read hands back a checkpoint too: the model never has to keep the one execute returned
+ assert recovered["checkpoint"]["receiptId"] == "at1fake1"
+ assert recovered["checkpoint"]["route"]["id"] == out["checkpoint"]["route"]["id"]
+ assert b.submitted.count(b.submitted[0]) == 1 # recover never rebroadcasts
+ pending = dispatch_tool(b, "bridge_pending", {})
+ assert [p["progress"]["receipt"]["id"] for p in pending] == ["at1fake1"]
+ assert [p["checkpoint"] for p in pending] == [cp.to_dict() for cp in store.list()]
+ json.dumps(pending)
+
+
+def test_pending_hands_back_the_stored_checkpoint_that_restarts_the_flow(tmp_path):
+ """The restart path: a fresh process lists pending transfers and resumes one from the
+ checkpoint the listing echoed — never a checkpoint re-derived from a recovered receipt."""
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ cp, serialized = _aleo_out_checkpoint(b)
+ store.save(Checkpoint.from_dict(cp))
+
+ entries = dispatch_tool(b, "bridge_pending", {})
+ assert len(entries) == 1 and entries[0]["progress"]["next"] == "resume"
+ assert entries[0]["checkpoint"] == Checkpoint.from_dict(cp).to_dict() # stored form, verbatim
+ out = dispatch_tool(b, "bridge_resume", {"checkpoint": entries[0]["checkpoint"], "confirm": True})
+ assert out["progress"]["receipt"]["status"] == "SOURCE_CONFIRMING" and b.aleo.submitted == [serialized]
+
+
+def test_pending_checkpoint_completes_a_mint_whose_offline_progress_still_says_wait(tmp_path):
+ """``bridge_pending`` is offline, so its progress can lag the chain (here: 'wait' while Circle
+ has in fact attested). The echoed checkpoint is still exactly what ``bridge_complete`` takes."""
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(environment="testnet", checkpoints=store)
+ cp = _inbound_private_checkpoint(b)
+ store.save(Checkpoint.from_dict(cp))
+
+ entries = dispatch_tool(b, "bridge_pending", {})
+ assert len(entries) == 1 and entries[0]["progress"]["next"] == "wait" # offline view
+ out = dispatch_tool(b, "bridge_complete", {"checkpoint": entries[0]["checkpoint"],
+ "secret_nonce": NONCE, "confirm": True})
+ assert out["progress"]["receipt"]["status"] == "DESTINATION_CONFIRMING"
+ assert NONCE not in json.dumps(out)
+
+
+def test_pending_reports_a_malformed_record_instead_of_collapsing_the_list(tmp_path):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ cp, _ = _aleo_out_checkpoint(b)
+ store.save(Checkpoint.from_dict(cp))
+ (tmp_path / "at1broken.json").write_text(json.dumps(
+ {"version": 1, "receiptId": "at1broken", "intent": {}, "route": {"id": "x", "registryVersion": "y"}}),
+ encoding="utf-8")
+
+ entries = dispatch_tool(b, "bridge_pending", {})
+ assert len(entries) == 2
+ healthy = [e for e in entries if "progress" in e]
+ broken = [e for e in entries if "error" in e]
+ assert len(healthy) == 1 and healthy[0]["checkpoint"] == Checkpoint.from_dict(cp).to_dict()
+ assert len(broken) == 1 and broken[0]["checkpoint_id"] == "at1broken"
+ assert broken[0]["error_type"] == "CheckpointInvalidError" and broken[0]["error"]
+ assert "progress" not in broken[0]
+ json.dumps(entries)
+
+
+def test_checkpoint_property_names_exactly_the_tools_that_return_one():
+ tools = {t["name"]: t for t in bridge_tools()}
+ description = tools["bridge_get_progress"]["input_schema"]["properties"]["checkpoint"]["description"]
+ returns_one = {"bridge_execute", "bridge_resume", "bridge_complete", "bridge_get_progress", "bridge_pending"}
+ assert {name for name in tools if name in description} == returns_one
+
+
+def test_resume_and_complete_gates():
+ b = FakeBridge(ethereum=False)
+ cp, serialized = _aleo_out_checkpoint(b)
+ out = dispatch_tool(b, "bridge_resume", {"checkpoint": cp})
+ assert out["confirmation_required"] is True and out["progress"]["next"] == "resume" and b.aleo.submitted == []
+ out = dispatch_tool(b, "bridge_resume", {"checkpoint": cp, "confirm": True})
+ # a resumed Aleo leg rebroadcasts the checkpointed bytes through the network, never re-proving
+ assert out["progress"]["receipt"]["status"] == "SOURCE_CONFIRMING" and b.aleo.submitted == [serialized]
+
+ b2 = FakeBridge(environment="testnet")
+ cp2 = _inbound_private_checkpoint(b2)
+ out = dispatch_tool(b2, "bridge_complete", {"checkpoint": cp2, "secret_nonce": NONCE})
+ assert out["confirmation_required"] is True and out["progress"]["next"] == "complete" and b2.events == []
+ out = dispatch_tool(b2, "bridge_complete", {"checkpoint": cp2, "secret_nonce": NONCE, "confirm": True})
+ assert out["progress"]["receipt"]["status"] == "DESTINATION_CONFIRMING"
+ assert NONCE not in json.dumps(out)
+
+
+def test_shield_unshield_gates():
+ b = FakeBridge()
+ out = dispatch_tool(b, "bridge_shield", {"asset": "aleo/eth", "amount": "0.000000000000000001"})
+ assert out["confirmation_required"] is True and out["call"]["function"] == "shield" and b.events == []
+ out = dispatch_tool(b, "bridge_shield", {"asset": "aleo/eth", "amount": "0.000000000000000001", "confirm": True})
+ assert out["direction"] == "shield" and b.events[-1][0] == "delegate"
+ out = dispatch_tool(b, "bridge_unshield", {"asset": "aleo/eth", "amount_atomic": 1, "confirm": True})
+ assert out["direction"] == "unshield" and out["amount_atomic"] == 1
+
+
+def test_privacy_previews_never_echo_a_record(tmp_path):
+ """An unshield call's inputs carry the selected record's PLAINTEXT — the private balance
+ itself. The preview an agent shows a user must summarize it, never echo it."""
+ b = FakeBridge()
+ out = dispatch_tool(b, "bridge_unshield", {"asset": "aleo/eth", "amount_atomic": 1})
+ blob = json.dumps(out)
+ assert out["confirmation_required"] is True
+ assert out["call"] == {"program": "arc20_eth.aleo", "function": "unshield",
+ "inputs": ["", "1u128"]} # amount literal kept
+ assert RECORD_PLAINTEXT not in blob and "_nonce" not in blob and "owner:" not in blob
+ # shielding has no record to leak: its literals pass through untouched
+ shield = dispatch_tool(b, "bridge_shield", {"asset": "aleo/eth", "amount_atomic": 1})
+ assert shield["call"]["inputs"] == ["1u128"]
+
+
+@pytest.mark.parametrize("value", [
+ RECORD_PLAINTEXT,
+ "{ owner: aleo1abc.private, microcredits: 1500000u64.private }",
+ "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzq"
+ "8tc0y3cc45vqs0vzcqmqxqwqsy3y6qw6w4vdkvq3qsqrqwqsyq",
+ "at1abcdefghijklmnop.record",
+])
+def test_summarizer_redacts_record_shaped_inputs(value):
+ assert _summarize_input(value) == ""
+
+
+@pytest.mark.parametrize("value", ["1u128", ALEO_RECIPIENT, "arc20_eth.aleo", "transfer_private_to_public",
+ "{ siblings: [ 0field ], leaf_index: 0u32 }", "true", "0u64"])
+def test_summarizer_keeps_public_literals(value):
+ assert _summarize_input(value) == value
+
+
+def test_bridge_tools_returns_deep_copies():
+ first = bridge_tools()
+ first[0]["description"] = "mutated"
+ first[0]["input_schema"]["properties"]["injected"] = {"type": "string"}
+ quote = next(t for t in first if t["name"] == "bridge_quote")
+ quote["input_schema"]["required"].append("injected")
+
+ second = bridge_tools()
+ assert second[0]["description"] != "mutated"
+ assert "injected" not in second[0]["input_schema"]["properties"]
+ fresh_quote = next(t for t in second if t["name"] == "bridge_quote")
+ assert fresh_quote["input_schema"]["required"] == ["source", "destination", "amount", "recipient"]
+ assert fresh_quote["input_schema"]["properties"] is not quote["input_schema"]["properties"]
+
+
+def test_unknown_tool():
+ with pytest.raises(ValueError, match="Unknown bridge tool"):
+ dispatch_tool(FakeBridge(), "nope", {})
+
+
+# ── controller rulings ────────────────────────────────────────────────────────
+
+def test_private_mint_never_defaults_the_secret_nonce():
+ b = FakeBridge()
+ private = {**QUOTE_ARGS, "mint_mode": "private"}
+ for tool in ("bridge_quote", "bridge_execute"):
+ out = dispatch_tool(b, tool, dict(private))
+ assert "secret_nonce" in out["error"] and out["how_to_fix"]
+ assert "confirmation_required" not in out and "quote" not in out
+ out = dispatch_tool(b, "bridge_execute", {**private, "confirm": True})
+ assert "secret_nonce" in out["error"]
+ assert b.calls == [] and b.events == [] # nothing quoted, nothing moved
+ # a public mint still defaults quietly
+ assert dispatch_tool(b, "bridge_quote", QUOTE_ARGS)["kind"] == "evm-xreserve"
+
+ b2 = FakeBridge(environment="testnet")
+ cp2 = _inbound_private_checkpoint(b2)
+ out = dispatch_tool(b2, "bridge_complete", {"checkpoint": cp2, "confirm": True})
+ assert "secret_nonce" in out["error"] and out["how_to_fix"] and b2.events == []
+
+
+def test_no_write_tool_ever_echoes_the_secret_nonce():
+ """Every write tool, with and without confirm: the nonce is not in the JSON result."""
+ results = []
+
+ b = FakeBridge()
+ private = {**QUOTE_ARGS, "mint_mode": "private", "secret_nonce": NONCE}
+ results.append(dispatch_tool(b, "bridge_execute", dict(private)))
+ results.append(dispatch_tool(b, "bridge_execute", {**private, "confirm": True}))
+ results.append(dispatch_tool(b, "bridge_shield", {"asset": "aleo/eth", "amount_atomic": 1,
+ "secret_nonce": NONCE}))
+ results.append(dispatch_tool(b, "bridge_shield", {"asset": "aleo/eth", "amount_atomic": 1,
+ "secret_nonce": NONCE, "confirm": True}))
+ results.append(dispatch_tool(b, "bridge_unshield", {"asset": "aleo/eth", "amount_atomic": 1,
+ "secret_nonce": NONCE}))
+ results.append(dispatch_tool(b, "bridge_unshield", {"asset": "aleo/eth", "amount_atomic": 1,
+ "secret_nonce": NONCE, "confirm": True}))
+
+ b2 = FakeBridge(ethereum=False)
+ cp, _ = _aleo_out_checkpoint(b2)
+ results.append(dispatch_tool(b2, "bridge_resume", {"checkpoint": cp, "secret_nonce": NONCE}))
+ results.append(dispatch_tool(b2, "bridge_resume", {"checkpoint": cp, "secret_nonce": NONCE, "confirm": True}))
+
+ b3 = FakeBridge(environment="testnet")
+ cp2 = _inbound_private_checkpoint(b3)
+ results.append(dispatch_tool(b3, "bridge_complete", {"checkpoint": cp2, "secret_nonce": NONCE}))
+ results.append(dispatch_tool(b3, "bridge_complete", {"checkpoint": cp2, "secret_nonce": NONCE, "confirm": True}))
+
+ assert len(results) == 10
+ for result in results:
+ blob = json.dumps(result)
+ assert NONCE not in blob and "secretNonce" not in blob and "secret_nonce" not in blob
+
+
+def test_unconfigured_chain_returns_a_structured_error():
+ b = FakeBridge(ethereum=False) # no EVM connection
+ for tool, args in (("bridge_quote", QUOTE_ARGS), ("bridge_execute", {**QUOTE_ARGS, "confirm": True})):
+ out = dispatch_tool(b, tool, dict(args))
+ assert out["error"] and out["error_type"] in {"ConfigurationError", "BridgeError"}
+ assert "EVM_PRIVATE_KEY" in out["how_to_fix"] and "ETHEREUM_RPC_URL" in out["how_to_fix"]
+ assert "next" not in out
+ assert b.calls == [] and b.events == []
+ out = dispatch_tool(FakeBridge(solana=False), "bridge_quote",
+ {"source": "solana/sol", "destination": "aleo/sol", "amount": "0.1",
+ "recipient": ALEO_RECIPIENT})
+ assert "SOLANA_PRIVATE_KEY" in out["how_to_fix"]
+
+
+def test_bridge_error_from_a_read_is_structured_too():
+ out = dispatch_tool(FakeBridge(), "bridge_quote", {**QUOTE_ARGS, "amount": "0"})
+ assert out["error"] and out["error_type"] == "InvalidAmountError"
+
+
+def test_ambiguous_send_surfaces_recover_guidance_and_the_checkpoint():
+ b = FakeBridge()
+ route_id = "xreserve:ethereum/usdc->aleo/usdcx"
+ approval = Receipt(id="0x" + "11" * 32, protocol="xreserve", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": route_id, "approvalTxIds": ["0x" + "11" * 32],
+ "sourceSender": EVM_ADDRESS})
+ b.eth.intermediates = [approval]
+ b.eth.send_error = BridgeError("Ethereum deposit 0x" + "bb" * 32 + " may already be broadcast: "
+ "the RPC response was lost")
+ out = dispatch_tool(b, "bridge_execute", {**QUOTE_ARGS, "confirm": True})
+ assert out["error_type"] == "BridgeError" and "may already be broadcast" in out["error"]
+ assert out["next"] == "recover" and "bridge_get_progress" in out["how_to_fix"]
+ assert out["checkpoint"]["receiptId"] == "0x" + "11" * 32
+ assert out["checkpoint"]["route"]["id"] == route_id
+ json.dumps(out)
+
+
+def test_resume_write_error_surfaces_recover_guidance_and_the_checkpoint():
+ b = FakeBridge()
+ plan = prepare(b.registry, source="ethereum/usdc", destination="aleo/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, sender=EVM_ADDRESS, mint_mode="private")
+ approval = "0x" + "11" * 32
+ receipt = Receipt(id=approval, protocol="xreserve", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [approval],
+ "sourceSender": EVM_ADDRESS,
+ "hookData": "0x" + b.eth.hook_data.hex()})
+ b.eth.recover_result = receipt # history still has no deposit
+ b.eth.intermediates = [receipt] # re-checkpointed before the deposit goes out
+ b.eth.send_error = BridgeError("Ethereum deposit may already be broadcast: the RPC response was lost")
+ cp = create_checkpoint(plan, receipt, b.registry).to_dict()
+
+ out = dispatch_tool(b, "bridge_resume", {"checkpoint": cp, "secret_nonce": NONCE, "confirm": True})
+ assert out["error_type"] == "BridgeError" and "may already be broadcast" in out["error"]
+ assert out["next"] == "recover" and "bridge_get_progress" in out["how_to_fix"]
+ assert out["checkpoint"]["receiptId"] == approval and out["checkpoint"]["route"]["id"] == plan.route_id
+ assert NONCE not in json.dumps(out)
+
+
+def test_complete_write_error_surfaces_recover_guidance_and_the_prepared_checkpoint():
+ b = FakeBridge(environment="testnet")
+ cp = _inbound_private_checkpoint(b)
+ b.submit_error = BridgeError("Aleo mint at1fake1 may already be broadcast: the node answer was lost")
+
+ out = dispatch_tool(b, "bridge_complete", {"checkpoint": cp, "secret_nonce": NONCE, "confirm": True})
+ assert out["error_type"] == "BridgeError" and out["next"] == "recover"
+ assert "bridge_get_progress" in out["how_to_fix"]
+ # the pre-broadcast checkpoint made it out: the proved mint can be rebroadcast, never re-proved
+ assert out["checkpoint"]["destination"]["preparedTransaction"]["transactionId"] == "at1fake1"
+ assert NONCE not in json.dumps(out)
diff --git a/bridge-sdk/tests/test_base58.py b/bridge-sdk/tests/test_base58.py
new file mode 100644
index 0000000..427336f
--- /dev/null
+++ b/bridge-sdk/tests/test_base58.py
@@ -0,0 +1,29 @@
+import pytest
+
+from aleo_bridge._base58 import b58decode, b58encode
+
+SYSTEM_PROGRAM = "11111111111111111111111111111111"
+WARP_PROGRAM = "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7"
+
+
+def test_decode_known_solana_keys():
+ assert b58decode(SYSTEM_PROGRAM) == bytes(32)
+ raw = b58decode(WARP_PROGRAM)
+ assert len(raw) == 32
+ # First four bytes match the aleoRemoteRouterRecipient literal of hyperlane:aleo/sol->solana/sol
+ assert list(raw[:4]) == [112, 4, 72, 22]
+ assert list(raw[-4:]) == [7, 6, 211, 122]
+
+
+def test_round_trip_and_leading_zero_handling():
+ for value in (bytes(32), b"\x00\x00\x01", b"\x01\x00\x00", bytes(range(1, 33)), b""):
+ assert b58decode(b58encode(value)) == value
+ assert b58encode(b"") == ""
+ assert b58encode(bytes(32)) == SYSTEM_PROGRAM
+ assert b58encode(b58decode(WARP_PROGRAM)) == WARP_PROGRAM
+
+
+def test_decode_rejects_bad_characters():
+ for bad in ("not-base58!", "0OIl", "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7 "):
+ with pytest.raises(ValueError):
+ b58decode(bad)
diff --git a/bridge-sdk/tests/test_bridge_eth_wiring.py b/bridge-sdk/tests/test_bridge_eth_wiring.py
new file mode 100644
index 0000000..3582109
--- /dev/null
+++ b/bridge-sdk/tests/test_bridge_eth_wiring.py
@@ -0,0 +1,105 @@
+import pytest
+from eth_account import Account
+from web3 import HTTPProvider, Web3
+
+from aleo_bridge import Bridge, Ethereum, EthModule, EvmCall
+from aleo_bridge.errors import ChainMismatchError, ConfigurationError
+from aleo_bridge.types import BridgeStatus, ChainStatus
+from tests.fakes.fake_web3 import fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+WBTC, USDC, USDT = ("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "0xdAC17F958D2ee523a2206206994597C13D831ec7")
+
+
+def test_package_exports():
+ assert EthModule is not None and EvmCall is not None and Ethereum is not None
+
+
+def test_eth_property_requires_a_connection():
+ bridge = make_bridge()
+ assert bridge.ethereum is None
+ with pytest.raises(ConfigurationError,
+ match=r"Pass ethereum=Ethereum\(\.\.\.\) to Bridge\(\.\.\.\) or set EVM_PRIVATE_KEY \+ ETHEREUM_RPC_URL"):
+ bridge.eth
+
+
+def test_eth_property_is_cached_module_bound_to_connection():
+ conn = Ethereum(w3=fake_web3(), private_key=KEY)
+ bridge = make_bridge(ethereum=conn)
+ assert bridge.ethereum is conn and isinstance(bridge.eth, EthModule)
+ assert bridge.eth is bridge.eth and bridge.eth.conn is conn and bridge.eth.chain.id == "ethereum"
+ assert make_bridge(environment="testnet", ethereum=Ethereum(w3=fake_web3(chain_id=11155111))).eth.chain.id == "sepolia"
+
+
+def test_bare_web3_is_wrapped_read_only_unless_default_account():
+ w3 = fake_web3()
+ bridge = make_bridge(ethereum=w3)
+ assert isinstance(bridge.ethereum, Ethereum) and bridge.ethereum.w3 is w3 and not bridge.ethereum.can_sign
+ w3.eth.default_account = ACCT.address
+ assert make_bridge(ethereum=w3).ethereum.address == ACCT.address
+ with pytest.raises(ConfigurationError, match="Ethereum connection or a web3.Web3"):
+ make_bridge(ethereum="https://not-a-client")
+
+
+def test_from_env_requires_both_evm_variables(monkeypatch):
+ from aleo import PrivateKey
+
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", str(PrivateKey.random()))
+ monkeypatch.delenv("SOLANA_PRIVATE_KEY", raising=False)
+ monkeypatch.delenv("BRIDGE_CHECKPOINT_DIR", raising=False)
+ monkeypatch.delenv("BRIDGE_EVM_PRIVATE_KEY", raising=False)
+ monkeypatch.delenv("BRIDGE_LIVE_ETHEREUM_RPC_URL", raising=False)
+ monkeypatch.setenv("EVM_PRIVATE_KEY", KEY)
+ monkeypatch.delenv("ETHEREUM_RPC_URL", raising=False)
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Bridge.from_env()
+ monkeypatch.delenv("EVM_PRIVATE_KEY")
+ monkeypatch.setenv("ETHEREUM_RPC_URL", "http://127.0.0.1:1")
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Bridge.from_env()
+ monkeypatch.setenv("EVM_PRIVATE_KEY", KEY)
+ bridge = Bridge.from_env()
+ assert bridge.ethereum.address == ACCT.address and isinstance(bridge.ethereum.w3.provider, HTTPProvider)
+ assert bridge.ethereum.w3.provider.endpoint_uri == "http://127.0.0.1:1"
+ override = Ethereum(w3=fake_web3())
+ assert Bridge.from_env(ethereum=override).ethereum is override
+ monkeypatch.delenv("EVM_PRIVATE_KEY")
+ monkeypatch.delenv("ETHEREUM_RPC_URL")
+ assert Bridge.from_env().ethereum is None
+
+
+def test_chain_status_reads_native_and_erc20_balances():
+ w3 = fake_web3(eth_balances={ACCT.address: 5}, token_balances={(WBTC, ACCT.address): 7, (USDC, ACCT.address): 2_000_000})
+ eth = make_bridge(ethereum=Ethereum(w3=w3, private_key=KEY)).eth
+ status = eth.chain_status()
+ assert isinstance(status, ChainStatus) and status.chain_id == "ethereum" and status.address == ACCT.address and status.can_sign
+ assert status.balances == {"ethereum/eth": 5, "ethereum/usdc": 2_000_000, "ethereum/wbtc": 7, "ethereum/usdt": 0}
+ read_only = make_bridge(ethereum=Ethereum(w3=fake_web3())).eth.chain_status()
+ assert read_only.address is None and not read_only.can_sign and read_only.balances == {}
+
+
+def test_chain_status_asserts_the_connected_chain(monkeypatch):
+ """A Web3 pointed at the wrong network must fail chain_status() (and therefore Bridge.status())
+ with ChainMismatchError before any balance is read."""
+ w3 = fake_web3(chain_id=999)
+ bridge = make_bridge(ethereum=Ethereum(w3=w3))
+ with pytest.raises(ChainMismatchError, match="expected 1"):
+ bridge.eth.chain_status()
+ assert "eth_getBalance" not in w3.provider.methods and "eth_call" not in w3.provider.methods
+ aleo_status = ChainStatus(chain_id="aleo", address=None, can_sign=False, balances={})
+ monkeypatch.setattr(Bridge, "_aleo_chain_status", lambda self: aleo_status)
+ with pytest.raises(ChainMismatchError, match="expected 1"):
+ bridge.status()
+
+
+def test_bridge_status_includes_evm_chain(monkeypatch):
+ aleo_status = ChainStatus(chain_id="aleo", address="aleo1" + "q" * 58, can_sign=True, balances={})
+ monkeypatch.setattr(Bridge, "_aleo_chain_status", lambda self: aleo_status)
+ w3 = fake_web3(eth_balances={ACCT.address: 5})
+ bridge = make_bridge(ethereum=Ethereum(w3=w3, private_key=KEY))
+ status = bridge.status()
+ assert isinstance(status, BridgeStatus) and [c.chain_id for c in status.chains] == ["aleo", "ethereum"]
+ assert status.chains[1].balances["ethereum/eth"] == 5
+ assert [c.chain_id for c in make_bridge().status().chains] == ["aleo"]
diff --git a/bridge-sdk/tests/test_calls.py b/bridge-sdk/tests/test_calls.py
new file mode 100644
index 0000000..d8a21c8
--- /dev/null
+++ b/bridge-sdk/tests/test_calls.py
@@ -0,0 +1,133 @@
+import json
+
+import pytest
+
+from aleo_bridge._calls import AleoCall, extract_tx_id, is_duplicate_submission, payload_transitions, root_outputs
+from aleo_bridge.errors import ConfigurationError
+from aleo_bridge.types import PreparedTx
+
+PROGRAM, FN = "shielded_usdcx_wrapper.aleo", "private_burn"
+
+
+def _call(fake_aleo, imports=None) -> AleoCall:
+ bound = fake_aleo.programs.get(PROGRAM).functions[FN]("a", "2500000u128")
+ return AleoCall(fake_aleo, bound, lambda tx_id, outs: (tx_id, outs), imports=imports)
+
+
+def test_helpers():
+ assert extract_tx_id("at1abc") == "at1abc"
+ assert extract_tx_id({"transaction": {"id": "at1x"}}) == "at1x"
+ assert extract_tx_id({"transaction_id": "at1y"}) == "at1y"
+ with pytest.raises(ValueError):
+ extract_tx_id({"nope": 1})
+ decoded = [{"program": "tok.aleo", "function": "transfer", "outputs": [{"value": "999field"}]},
+ {"program": "p.aleo", "function": "f", "outputs": [{"value": "77field"}]},
+ {"program": "p.aleo", "function": "f", "outputs": ["78field"]}]
+ assert root_outputs(decoded, "p.aleo", "f") == ["78field"] # LAST matching transition is the root
+ assert root_outputs(decoded, "p.aleo", "g") == []
+ assert payload_transitions({"transaction_id": "x"}) is None
+ assert payload_transitions({"transaction": {"execution": {"transitions": [{"program": "p", "function": "f", "outputs": []}]}}}) == \
+ [{"program": "p", "function": "f", "outputs": []}]
+ assert is_duplicate_submission(RuntimeError("Transaction 'at1x' already exists in the ledger"))
+ assert not is_duplicate_submission(RuntimeError("duplicate transaction")) # real double-spend, must propagate
+ assert not is_duplicate_submission(RuntimeError("duplicate serial number"))
+ assert not is_duplicate_submission(RuntimeError("insufficient fee"))
+
+
+def test_attributes_and_simulate(fake_aleo):
+ call = _call(fake_aleo)
+ assert (call.program_id, call.function_name, call.inputs) == (PROGRAM, FN, ["a", "2500000u128"])
+ assert call.simulate() == "simulated" and fake_aleo.simulated == [(PROGRAM, FN)]
+ assert fake_aleo.submitted == [] and fake_aleo.delegated == []
+
+
+def test_repr_never_leaks_input_literals(fake_aleo):
+ # private_burn input 0 is a USDCx record plaintext; a secret nonce (private_mint input 3) is
+ # just as sensitive — repr() must never print .inputs, only a count.
+ record = "{ owner: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px.private, amount: 5000000u128.private, _nonce: 7group.public }"
+ secret_nonce = "7scalar"
+ bound = fake_aleo.programs.get(PROGRAM).functions[FN](record, "2500000u128", "0u32", "[0field]", secret_nonce)
+ call = AleoCall(fake_aleo, bound, lambda tx_id, outs: (tx_id, outs))
+ text = repr(call)
+ assert text == f"AleoCall({PROGRAM}/{FN}, inputs=5 literals)"
+ assert record not in text and secret_nonce not in text
+ assert call.inputs == [record, "2500000u128", "0u32", "[0field]", secret_nonce] # accessor still exposes them
+
+
+def test_prove_returns_prepared_tx_without_broadcast(fake_aleo):
+ prepared = _call(fake_aleo).prove(priority_fee=5)
+ assert isinstance(prepared, PreparedTx) and prepared.transaction_id == "at1built"
+ assert json.loads(prepared.serialized)["id"] == "at1built"
+ assert fake_aleo.fee_kwargs == [{"priority_fee": 5}] and fake_aleo.submitted == []
+
+
+def test_transact_harvests_root_outputs_then_broadcasts(fake_aleo):
+ tx_id, outputs = _call(fake_aleo).transact()
+ assert tx_id == "at1built" and outputs == ["77field"]
+ assert len(fake_aleo.submitted) == 1 and json.loads(fake_aleo.submitted[0])["id"] == "at1built"
+
+
+def test_delegate_broadcast_uses_payload_transitions(fake_aleo):
+ tx_id, outputs = _call(fake_aleo).delegate(wait=False)
+ assert (tx_id, outputs) == ("at1delegated", ["77field"])
+ assert fake_aleo.delegated[0]["broadcast"] is True and fake_aleo.waited == []
+ _call(fake_aleo).delegate(wait_timeout=7.0)
+ assert fake_aleo.waited == [("at1delegated", 7.0)]
+
+
+def test_delegate_falls_back_to_fetching_when_payload_is_id_only(fake_aleo):
+ fake_aleo.delegate_returns_id_only = True
+ bound = fake_aleo.programs.get("hyp_warp_token_wbtc_v2.aleo").functions["transfer_remote"]("x")
+ tx_id, outputs = AleoCall(fake_aleo, bound, lambda t, o: (t, o)).delegate(wait=False)
+ assert tx_id == "at1delegated" and outputs == ["99field"] # from get_transaction_object
+ assert fake_aleo.waited == [("at1delegated", 180.0)] # must wait before fetching
+
+
+def test_delegate_prepared_and_submit_prepared(fake_aleo):
+ call = _call(fake_aleo)
+ prepared = call.delegate_prepared()
+ assert fake_aleo.delegated[-1]["broadcast"] is False and fake_aleo.submitted == []
+ assert prepared.transaction_id == "at1delegated" and json.loads(prepared.serialized)["execution"]["transitions"]
+ tx_id, outputs = call.submit_prepared(prepared, wait=False)
+ assert (tx_id, outputs) == ("at1delegated", ["77field"]) and fake_aleo.submitted == [prepared.serialized]
+
+
+def test_delegate_without_broadcast_is_prepare_then_submit(fake_aleo):
+ tx_id, outputs = _call(fake_aleo).delegate(broadcast=False, wait_timeout=9.0)
+ assert tx_id == "at1delegated" and outputs == ["77field"]
+ assert fake_aleo.delegated[-1]["broadcast"] is False and len(fake_aleo.submitted) == 1
+ assert fake_aleo.waited == [("at1delegated", 9.0)]
+
+
+def test_submit_prepared_treats_duplicate_as_success(fake_aleo):
+ call = _call(fake_aleo)
+ prepared = call.delegate_prepared()
+ fake_aleo.duplicate_on_submit = True
+ tx_id, outputs = call.submit_prepared(prepared)
+ assert tx_id == "at1delegated" and outputs == ["77field"]
+
+
+def test_submit_prepared_confirmation_timeout_propagates_after_broadcast(fake_aleo):
+ from aleo.facade.errors import TransactionConfirmationTimeout
+
+ call = _call(fake_aleo)
+ prepared = call.delegate_prepared()
+ fake_aleo.wait_raises = True
+ with pytest.raises(TransactionConfirmationTimeout):
+ call.submit_prepared(prepared)
+ assert fake_aleo.submitted == [prepared.serialized] # broadcast already happened
+
+
+def test_delegate_prepared_requires_transaction_payload(fake_aleo):
+ fake_aleo.delegate_returns_id_only = True
+ with pytest.raises(ConfigurationError, match="did not return the transaction"):
+ _call(fake_aleo).delegate_prepared()
+
+
+def test_imports_are_registered_once_before_first_verb(fake_aleo):
+ call = _call(fake_aleo, imports={"token_registry.aleo": "program token_registry.aleo;", PROGRAM: "program shielded_usdcx_wrapper.aleo;"})
+ assert fake_aleo.registered == []
+ call.simulate()
+ assert fake_aleo.registered == ["token_registry.aleo", PROGRAM]
+ call.simulate()
+ assert fake_aleo.registered == ["token_registry.aleo", PROGRAM] # idempotent
diff --git a/bridge-sdk/tests/test_checkpoint.py b/bridge-sdk/tests/test_checkpoint.py
new file mode 100644
index 0000000..edce322
--- /dev/null
+++ b/bridge-sdk/tests/test_checkpoint.py
@@ -0,0 +1,219 @@
+"""Tests for the version-1 checkpoint allowlist and FileCheckpointStore (brief §2.10).
+
+``lifecycle.prepare`` does not exist yet (plan 4 task order runs checkpoint.py before
+lifecycle.py, since EvmCall.send/SolCall.send depend on create_checkpoint) — plans are
+built by hand from the real DEFAULT_REGISTRY instead of going through prepare().
+"""
+import json
+import os
+import stat
+import time
+from decimal import Decimal
+
+import pytest
+
+from aleo_bridge.checkpoint import Checkpoint, FileCheckpointStore, create_checkpoint
+from aleo_bridge.errors import CheckpointInvalidError
+from aleo_bridge.registry import DEFAULT_REGISTRY, Registry
+from aleo_bridge.types import Plan, Receipt, Status
+
+RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+APPROVAL = "0x" + "11" * 32
+SOURCE = "0x" + "22" * 32
+EVM1 = "0x0000000000000000000000000000000000000001"
+
+
+def _make_plan(registry: Registry, *, source, destination, amount, recipient,
+ sender=None, protocol=None, mint_mode="public") -> Plan:
+ route = registry.find_route(source, destination, protocol=protocol)
+ src = registry.asset(source)
+ dst = registry.asset(destination)
+ amount_atomic = int(Decimal(amount) * (10 ** src.decimals))
+ return Plan(route_id=route.id, registry_version=registry.version, protocol=route.protocol,
+ environment=route.environment, source_asset_id=src.id, destination_asset_id=dst.id,
+ amount=amount, amount_atomic=amount_atomic, recipient=recipient, sender=sender,
+ mint_mode=mint_mode, steps=())
+
+
+def _plan(**kw):
+ base = dict(source="sepolia/usdc", destination="aleo-testnet/usdcx", amount="2",
+ recipient=RECIPIENT, mint_mode="private")
+ base.update(kw)
+ return _make_plan(DEFAULT_REGISTRY, **base)
+
+
+def test_allowlist_persists_intent_and_ids_only():
+ plan = _plan()
+ receipt = Receipt(id="at1destination", protocol="xreserve", status=Status.DESTINATION_CONFIRMING,
+ source_tx_id=SOURCE, destination_tx_id="at1destination",
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [APPROVAL],
+ "payload": "0xdeadbeef", "messageHash": "0x" + "33" * 32,
+ "nonce": "0x" + "44" * 32, "attestation": "0x" + "55" * 65,
+ "amountAtomic": "2000000", "maxFeeAtomic": "100000",
+ "remoteRecipientBytes32": "0x" + "66" * 32,
+ "secretNonce": "7scalar"})
+ cp = create_checkpoint(plan, receipt, DEFAULT_REGISTRY)
+ assert cp.to_dict() == {
+ "version": 1,
+ "receiptId": "at1destination",
+ "intent": {"source": {"chain": "sepolia", "asset": "usdc"},
+ "destination": {"chain": "aleo-testnet", "asset": "usdcx"},
+ "bridgeProtocol": "xreserve", "amount": "2", "recipient": RECIPIENT,
+ "mintMode": "private"},
+ "route": {"id": plan.route_id, "registryVersion": plan.registry_version},
+ "source": {"approvalTransactionIds": [APPROVAL], "transactionId": SOURCE},
+ "destination": {"transactionId": "at1destination"},
+ }
+ text = cp.to_json()
+ for forbidden in ("payload", "messageHash", "nonce", "attestation", "secretNonce",
+ "amountAtomic", "maxFeeAtomic", "remoteRecipientBytes32", "0xdeadbeef"):
+ assert forbidden not in text
+ assert cp.id == "at1destination"
+ assert Checkpoint.from_json(text) == cp
+
+
+def test_mint_mode_only_when_destination_is_an_aleo_program():
+ outbound = _make_plan(DEFAULT_REGISTRY, source="aleo/wbtc", destination="ethereum/wbtc",
+ amount="0.1", recipient=EVM1)
+ cp = create_checkpoint(outbound, Receipt(id="at1x", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="at1x", protocol_state={"routeId": outbound.route_id}),
+ DEFAULT_REGISTRY)
+ assert "mintMode" not in cp.intent and cp.intent["bridgeProtocol"] == "hyperlane"
+ assert cp.source == {"transactionId": "at1x"} and cp.destination is None
+ assert "destination" not in cp.to_dict()
+
+
+def test_sender_from_plan_or_source_sender():
+ plan = _plan(sender=EVM1)
+ receipt = Receipt(id=APPROVAL, protocol="xreserve", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [APPROVAL],
+ "sourceSender": "0x0000000000000000000000000000000000000002"})
+ assert create_checkpoint(plan, receipt, DEFAULT_REGISTRY).intent["sender"] == EVM1
+ plan2 = _plan()
+ assert create_checkpoint(plan2, receipt.replace(protocol_state={**receipt.protocol_state, "routeId": plan2.route_id}),
+ DEFAULT_REGISTRY).intent["sender"] == "0x0000000000000000000000000000000000000002"
+ assert create_checkpoint(plan2, Receipt(id=SOURCE, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=SOURCE, protocol_state={"routeId": plan2.route_id}),
+ DEFAULT_REGISTRY).intent.get("sender") is None
+
+
+def test_solana_blockhash_pair_both_or_neither():
+ plan = _make_plan(DEFAULT_REGISTRY, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=RECIPIENT, sender="11111111111111111111111111111111")
+ ok = Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id="sig",
+ protocol_state={"routeId": plan.route_id, "blockhash": "recent", "lastValidBlockHeight": "123456789"})
+ assert create_checkpoint(plan, ok, DEFAULT_REGISTRY).source == {
+ "transactionId": "sig", "blockhash": "recent", "lastValidBlockHeight": "123456789"}
+ with pytest.raises(CheckpointInvalidError, match="blockhash"):
+ create_checkpoint(plan, ok.replace(protocol_state={"routeId": plan.route_id, "blockhash": "recent"}),
+ DEFAULT_REGISTRY)
+ with pytest.raises(CheckpointInvalidError, match="blockhash"):
+ create_checkpoint(plan, ok.replace(protocol_state={"routeId": plan.route_id, "blockhash": "recent",
+ "lastValidBlockHeight": "abc"}), DEFAULT_REGISTRY)
+
+
+def test_delivery_verification_pair_both_or_neither():
+ plan = _make_plan(DEFAULT_REGISTRY, source="aleo/sol", destination="solana/sol", amount="0.000000001",
+ recipient="11111111111111111111111111111111")
+ receipt = Receipt(id="at1s", protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id="at1s",
+ protocol_state={"routeId": plan.route_id, "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"})
+ cp = create_checkpoint(plan, receipt, DEFAULT_REGISTRY)
+ assert cp.delivery_verification == {"balanceBeforeAtomic": "100", "expectedIncreaseAtomic": "1"}
+ with pytest.raises(CheckpointInvalidError, match="verification"):
+ create_checkpoint(plan, receipt.replace(protocol_state={"routeId": plan.route_id,
+ "destinationBalanceBeforeAtomic": "100"}),
+ DEFAULT_REGISTRY)
+
+
+def test_prepared_transactions_and_hook_data():
+ plan = _make_plan(DEFAULT_REGISTRY, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=EVM1)
+ serialized = json.dumps({"type": "execute", "id": "at1prepared", "fee": {}})
+ cp = create_checkpoint(plan, Receipt(id="at1prepared", protocol="hyperlane",
+ status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id,
+ "preparedTransaction": serialized}),
+ DEFAULT_REGISTRY)
+ assert cp.source == {"preparedTransaction": {"transactionId": "at1prepared",
+ "serializedTransaction": serialized}}
+ with pytest.raises(CheckpointInvalidError, match="prepared transaction"):
+ create_checkpoint(plan, Receipt(id="x", protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "preparedTransaction": ""}),
+ DEFAULT_REGISTRY)
+ inbound = _plan()
+ cp2 = create_checkpoint(inbound, Receipt(id="at1mint", protocol="xreserve",
+ status=Status.DESTINATION_ACTION_REQUIRED, source_tx_id=SOURCE,
+ protocol_state={"routeId": inbound.route_id, "hookData": "0x02" + "00" * 64,
+ "preparedDestinationTransaction": serialized}),
+ DEFAULT_REGISTRY)
+ assert cp2.source == {"transactionId": SOURCE, "hookData": "0x02" + "00" * 64}
+ assert cp2.destination == {"preparedTransaction": {"transactionId": "at1mint",
+ "serializedTransaction": serialized}}
+
+
+def test_rejects_receipts_from_other_routes_and_bad_approvals():
+ plan = _plan()
+ with pytest.raises(CheckpointInvalidError, match="does not match"):
+ create_checkpoint(plan, Receipt(id=SOURCE, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": "xreserve:wrong/route"}), DEFAULT_REGISTRY)
+ with pytest.raises(CheckpointInvalidError, match="does not match"):
+ create_checkpoint(plan, Receipt(id=SOURCE, protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": plan.route_id}), DEFAULT_REGISTRY)
+ with pytest.raises(CheckpointInvalidError, match="approval"):
+ create_checkpoint(plan, Receipt(id=SOURCE, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [1, 2]}),
+ DEFAULT_REGISTRY)
+
+
+def test_from_dict_accepts_veil_shaped_checkpoints_without_receipt_id():
+ veil = {"version": 1,
+ "intent": {"source": {"chain": "aleo", "asset": "eth"}, "destination": {"chain": "ethereum", "asset": "eth"},
+ "bridgeProtocol": "hyperlane", "amount": "0.000000000000000001", "recipient": EVM1},
+ "route": {"id": "hyperlane:aleo/eth->ethereum/eth", "registryVersion": DEFAULT_REGISTRY.version},
+ "source": {"preparedTransaction": {"transactionId": "at1prepared", "serializedTransaction": "{}"}}}
+ cp = Checkpoint.from_dict(veil)
+ assert cp.id == "at1prepared" and cp.version == 1 and cp.delivery_verification is None
+ assert Checkpoint.from_dict({**veil, "source": {"transactionId": "at1src"}}).id == "at1src"
+ assert Checkpoint.from_dict({**veil, "source": {"approvalTransactionIds": [APPROVAL]}}).id == APPROVAL
+ with pytest.raises(CheckpointInvalidError):
+ Checkpoint.from_dict({"version": 2, "intent": {}, "route": {}})
+ with pytest.raises(CheckpointInvalidError):
+ Checkpoint.from_dict({**veil, "source": {}})
+
+
+def test_file_store_roundtrip_mode_and_atomic_rename(tmp_path):
+ store = FileCheckpointStore(tmp_path / "cps")
+ plan = _plan()
+ cp = create_checkpoint(plan, Receipt(id=SOURCE, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=SOURCE, protocol_state={"routeId": plan.route_id}),
+ DEFAULT_REGISTRY)
+ store.save(cp)
+ files = list((tmp_path / "cps").iterdir())
+ assert [f.name for f in files] == [f"{SOURCE}.json"]
+ assert stat.S_IMODE(os.stat(files[0]).st_mode) == 0o600
+ assert not list((tmp_path / "cps").glob("*.tmp"))
+ assert store.load(SOURCE) == cp
+ assert store.load("missing") is None
+ assert store.list() == [cp]
+ store.delete(SOURCE)
+ store.delete(SOURCE) # idempotent
+ assert store.list() == [] and store.load(SOURCE) is None
+
+
+def test_file_store_sanitizes_ids_and_orders_by_mtime(tmp_path):
+ store = FileCheckpointStore(str(tmp_path))
+ plan = _make_plan(DEFAULT_REGISTRY, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=EVM1)
+ a = create_checkpoint(plan, Receipt(id="at1a", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="at1a", protocol_state={"routeId": plan.route_id}), DEFAULT_REGISTRY)
+ weird = create_checkpoint(plan, Receipt(id="../evil id", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="../evil id", protocol_state={"routeId": plan.route_id}),
+ DEFAULT_REGISTRY)
+ store.save(a)
+ time.sleep(0.01) # keep mtimes ordered on coarse filesystems
+ store.save(weird)
+ names = sorted(p.name for p in tmp_path.iterdir())
+ assert names == ["___evil_id.json", "at1a.json"]
+ assert [c.id for c in store.list()] == ["at1a", "../evil id"] # oldest first
+ assert store.load("../evil id") == weird
diff --git a/bridge-sdk/tests/test_circle.py b/bridge-sdk/tests/test_circle.py
new file mode 100644
index 0000000..585647b
--- /dev/null
+++ b/bridge-sdk/tests/test_circle.py
@@ -0,0 +1,92 @@
+import requests
+import pytest
+
+from aleo_bridge._keccak import keccak256
+from aleo_bridge.circle import CircleClient
+from aleo_bridge.errors import AttestationError, ConfigurationError
+from aleo_bridge.types import Attestation
+
+BASE = "https://xreserve-api.circle.com/v1/attestations"
+PAYLOAD = bytes(305)
+HASH = keccak256(PAYLOAD)
+SIG = bytes.fromhex("11" * 65)
+
+
+class _Response:
+ def __init__(self, status_code, body=None):
+ self.status_code, self._body = status_code, body
+
+ def json(self):
+ return self._body
+
+
+class _BadJsonResponse:
+ """A 200 whose body isn't valid JSON (``.json()`` raises, as ``requests`` does)."""
+ status_code = 200
+
+ def json(self):
+ raise ValueError("Expecting value: line 1 column 1 (char 0)")
+
+
+class _Session:
+ def __init__(self, *responses):
+ self._responses, self.urls = list(responses), []
+
+ def get(self, url, timeout=None):
+ self.urls.append((url, timeout))
+ return self._responses.pop(0)
+
+
+class _RaisingSession:
+ """A session whose ``get`` raises a transport-level ``requests`` exception."""
+ def __init__(self, exc):
+ self._exc = exc
+
+ def get(self, url, timeout=None):
+ raise self._exc
+
+
+def _body(payload=PAYLOAD, signature=SIG, message_hash=HASH):
+ return {"attestation": {"payload": "0x" + payload.hex(), "attestation": "0x" + signature.hex(), "messageHash": "0x" + message_hash.hex()}}
+
+
+def test_404_is_pending_none():
+ session = _Session(_Response(404))
+ assert CircleClient(BASE, session=session, timeout=9).get_attestation("0x" + HASH.hex()) is None
+ assert session.urls == [(f"{BASE}/0x{HASH.hex()}", 9)]
+
+
+def test_complete_attestation_is_verified():
+ att = CircleClient(BASE, session=_Session(_Response(200, _body()))).get_attestation(HASH.hex()) # bare hex accepted
+ assert att == Attestation(payload=PAYLOAD, message_hash=HASH, attestation=SIG, status="complete")
+
+
+def test_rejects_http_errors_and_bad_bodies():
+ with pytest.raises(AttestationError, match="HTTP 500"):
+ CircleClient(BASE, session=_Session(_Response(500))).get_attestation("0x" + HASH.hex())
+ with pytest.raises(AttestationError, match="invalid response"):
+ CircleClient(BASE, session=_Session(_Response(200, {"attestation": {"payload": "zz"}}))).get_attestation("0x" + HASH.hex())
+ with pytest.raises(AttestationError, match="invalid response"):
+ CircleClient(BASE, session=_Session(_Response(200, {}))).get_attestation("0x" + HASH.hex())
+ with pytest.raises(AttestationError, match="different message hash"):
+ CircleClient(BASE, session=_Session(_Response(200, _body(message_hash=bytes(32))))).get_attestation("0x" + HASH.hex())
+ other = bytes.fromhex("01" * 305)
+ with pytest.raises(AttestationError, match="does not match the requested message hash"):
+ CircleClient(BASE, session=_Session(_Response(200, _body(payload=other)))).get_attestation("0x" + HASH.hex())
+ with pytest.raises(AttestationError, match="32-byte"):
+ CircleClient(BASE, session=_Session()).get_attestation("0x1234")
+ with pytest.raises(ConfigurationError, match="https"):
+ CircleClient("http://insecure.example")
+
+
+def test_wraps_transport_and_json_failures():
+ for exc in (requests.exceptions.ConnectionError("connection refused"), requests.exceptions.Timeout("timed out")):
+ session = _RaisingSession(exc)
+ with pytest.raises(AttestationError, match="Circle attester request failed") as excinfo:
+ CircleClient(BASE, session=session).get_attestation("0x" + HASH.hex())
+ assert excinfo.value.__cause__ is exc
+
+ session = _Session(_BadJsonResponse())
+ with pytest.raises(AttestationError, match="Circle attester request failed") as excinfo:
+ CircleClient(BASE, session=session).get_attestation("0x" + HASH.hex())
+ assert isinstance(excinfo.value.__cause__, ValueError)
diff --git a/bridge-sdk/tests/test_client.py b/bridge-sdk/tests/test_client.py
new file mode 100644
index 0000000..5a073a0
--- /dev/null
+++ b/bridge-sdk/tests/test_client.py
@@ -0,0 +1,231 @@
+import json
+
+import pytest
+from eth_account import Account as EthAccount
+
+from aleo.facade.errors import ProgramNotFound
+
+from aleo_bridge import Bridge, __main__ as cli
+from aleo_bridge._calls import AleoCall
+from aleo_bridge.errors import ConfigurationError
+from aleo_bridge.eth import Ethereum, EthModule
+from aleo_bridge.freezelist import FreezeList
+from aleo_bridge.hyperlane import HyperlaneModule
+from aleo_bridge.privacy import PrivacyModule
+from aleo_bridge.registry import DEFAULT_REGISTRY, Registry
+from aleo_bridge.types import BridgeStatus
+from aleo_bridge.xreserve import XReserveModule
+from tests.conftest import SIGNER, FakeAleo, default_mappings
+from tests.fakes.fake_web3 import fake_web3
+
+
+def test_construction_defaults_and_namespaces(fake_aleo):
+ bridge = Bridge(fake_aleo)
+ assert (bridge.environment, bridge.network, bridge.registry) == ("mainnet", "mainnet", DEFAULT_REGISTRY)
+ assert bridge.checkpoints is None and bridge.ethereum is None and bridge.solana is None and bridge.profile is None
+ assert isinstance(bridge.hyperlane, HyperlaneModule) and isinstance(bridge.xreserve, XReserveModule)
+ assert isinstance(bridge.freezelist, FreezeList) and isinstance(bridge.privacy, PrivacyModule)
+ assert bridge.aleo_chain().id == "aleo" and bridge.aleo_address() == SIGNER
+ assert Bridge(FakeAleo(network_name="testnet")).aleo_chain().id == "aleo-testnet"
+
+
+def test_construction_errors(fake_aleo):
+ with pytest.raises(ConfigurationError, match="does not match"):
+ Bridge(fake_aleo, environment="testnet")
+ with pytest.raises(ConfigurationError, match="mainnet or testnet"):
+ Bridge(FakeAleo(network_name="devnet"))
+ empty = Registry(DEFAULT_REGISTRY.version, DEFAULT_REGISTRY.chains(environment="testnet"),
+ DEFAULT_REGISTRY.assets(environment="testnet"), DEFAULT_REGISTRY.routes(environment="testnet"))
+ with pytest.raises(ConfigurationError, match="no chains for mainnet"):
+ Bridge(fake_aleo, registry=empty)
+ with pytest.raises(ConfigurationError, match="default_account"):
+ Bridge(FakeAleo(default_account=False)).aleo_address()
+
+
+def test_eth_property_wraps_connection_and_sol_property_now_wraps_a_bare_client(fake_aleo):
+ """Plan 3 (task 5) landed ``SolModule``, so ``bridge.sol`` on a configured connection now
+ succeeds instead of degrading to ``MissingExtraError`` (that fallback covered the window
+ before ``SolModule`` existed; ``sol.py``'s ``try/except ImportError`` around the import is
+ still exercised by ``test_sol_connection.py``'s no-solders scenarios)."""
+ bridge = Bridge(fake_aleo)
+ with pytest.raises(ConfigurationError, match="ethereum="):
+ bridge.eth
+ with pytest.raises(ConfigurationError, match="solana="):
+ bridge.sol
+ eth_module = Bridge(fake_aleo, ethereum=fake_web3()).eth # plan 2: real Ethereum wraps a bare Web3
+ assert isinstance(eth_module, EthModule)
+ pytest.importorskip("solders")
+ from aleo_bridge.sol import SolModule
+ from tests.fakes.fake_solana import FakeSolanaClient
+
+ sol_module = Bridge(fake_aleo, solana=FakeSolanaClient()).sol # plan 3: bare client is wrapped in Solana
+ assert isinstance(sol_module, SolModule)
+
+
+def test_program_cache_mapping_value_and_call_registration(fake_aleo):
+ fake_aleo.imports = {"hyp_warp_token_wbtc_v2.aleo": ["token_registry.aleo", "hyp_mailbox.aleo"], "hyp_mailbox.aleo": ["token_registry.aleo"]}
+ bridge = Bridge(fake_aleo)
+ assert bridge.program("credits.aleo") is bridge.program("credits.aleo") and fake_aleo.fetched.count("credits.aleo") == 1
+ assert bridge.mapping_value("credits.aleo", "account", SIGNER) == "2392443u64"
+ assert bridge.mapping_value("credits.aleo", "account", "aleo1nobody") is None
+ fake_aleo.mappings["credits.aleo"]["account"]["aleo1null"] = "null"
+ fake_aleo.mappings["credits.aleo"]["account"]["aleo1quoted"] = '"7u64"'
+ assert bridge.mapping_value("credits.aleo", "account", "aleo1null") is None
+ assert bridge.mapping_value("credits.aleo", "account", "aleo1quoted") == "7u64"
+ call = bridge._call("hyp_warp_token_wbtc_v2.aleo", "transfer_remote", ["1u128"], lambda tx, outs: tx)
+ assert isinstance(call, AleoCall) and fake_aleo.registered == []
+ call.simulate()
+ assert fake_aleo.registered == ["token_registry.aleo", "hyp_mailbox.aleo", "hyp_warp_token_wbtc_v2.aleo"] # dependencies first, root last
+
+
+def test_mapping_value_returns_none_for_missing_program(fake_aleo):
+ fake_aleo.missing_programs = {"missing.aleo"}
+ bridge = Bridge(fake_aleo)
+ assert bridge.mapping_value("missing.aleo", "balances", SIGNER) is None
+ with pytest.raises(ProgramNotFound):
+ bridge.program("missing.aleo")
+
+
+def test_amount_helpers_and_privacy_delegation(fake_aleo):
+ from aleo_bridge.freezelist import EMPTY_TREE_ROOT
+
+ # unshield()'s default (unsupplied) merkle_proof= resolves through freezelist.exclusion_proof(),
+ # which now requires a readable on-chain root (item 4) — seed the empty-list root.
+ fake_aleo.mappings.setdefault("usdcx_freezelist.aleo", {})["freeze_list_root"] = {"1u8": f"{EMPTY_TREE_ROOT}field"}
+ bridge = Bridge(fake_aleo)
+ assert bridge.to_atomic("0.001", "aleo/wbtc") == 100_000 and bridge.from_atomic(100_000, ("aleo", "wbtc")) == "0.001"
+ assert bridge.to_atomic("1", DEFAULT_REGISTRY.asset("ethereum/usdc")) == 1_000_000
+ assert bridge.shield("aleo/eth", amount="1").function_name == "shield"
+ assert bridge.unshield("aleo/usdcx", amount="2.5").function_name == "transfer_private_to_public"
+
+
+def test_status_reads_every_aleo_asset_balance(fake_aleo):
+ status = Bridge(fake_aleo).status()
+ assert isinstance(status, BridgeStatus) and status.environment == "mainnet" and status.registry_version == DEFAULT_REGISTRY.version
+ assert status.pending == [] and len(status.chains) == 1
+ chain = status.chains[0]
+ assert (chain.chain_id, chain.address, chain.can_sign) == ("aleo", SIGNER, True)
+ assert chain.balances == {"aleo/aleo": 2392443, "aleo/usdcx": 1000000, "aleo/eth": 0, "aleo/wbtc": 10000,
+ "aleo/usdt": 0, "aleo/sol": 0, "aleo/usad": 0}
+ assert "arc20_usdt.aleo" in fake_aleo.fetched and "usad_stablecoin.aleo" in fake_aleo.fetched
+ unsigned = Bridge(FakeAleo(mappings=default_mappings(), default_account=False)).status().chains[0]
+ assert (unsigned.address, unsigned.can_sign) == (None, False) and set(unsigned.balances.values()) == {0}
+
+
+def test_from_env_builds_aleo_only(monkeypatch, fake_aleo):
+ captured = {}
+
+ def fake_build(endpoint, network, private_key, *, api_key=None, consumer_id=None):
+ captured.update(endpoint=endpoint, network=network, private_key=private_key, api_key=api_key, consumer_id=consumer_id)
+ return FakeAleo(mappings=default_mappings(), network_name=network)
+
+ monkeypatch.setattr("aleo_bridge.client.build_aleo", fake_build)
+ for var in ("BRIDGE_PRIVATE_KEY", "ALEO_ENDPOINT", "ALEO_NETWORK", "ALEO_API_KEY", "ALEO_CONSUMER_ID", "EVM_PRIVATE_KEY",
+ "ETHEREUM_RPC_URL", "BRIDGE_EVM_PRIVATE_KEY", "BRIDGE_LIVE_ETHEREUM_RPC_URL",
+ "SOLANA_PRIVATE_KEY", "SOLANA_RPC_URL", "BRIDGE_SOLANA_PRIVATE_KEY", "BRIDGE_LIVE_SOLANA_RPC_URL",
+ "BRIDGE_CHECKPOINT_DIR"):
+ monkeypatch.delenv(var, raising=False)
+ with pytest.raises(ConfigurationError, match="BRIDGE_PRIVATE_KEY"):
+ Bridge.from_env()
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", "APrivateKey1zkpTest")
+ bridge = Bridge.from_env()
+ assert captured == {"endpoint": "https://edge.provable.com/api", "network": "mainnet", "private_key": "APrivateKey1zkpTest",
+ "api_key": None, "consumer_id": None}
+ assert bridge.environment == "mainnet" and bridge.ethereum is None and bridge.solana is None and bridge.checkpoints is None
+ monkeypatch.setenv("ALEO_NETWORK", "testnet")
+ monkeypatch.setenv("ALEO_ENDPOINT", "https://api.provable.com/v2")
+ monkeypatch.setenv("ALEO_API_KEY", "k")
+ monkeypatch.setenv("ALEO_CONSUMER_ID", "c")
+ assert Bridge.from_env().environment == "testnet"
+ assert (captured["endpoint"], captured["api_key"], captured["consumer_id"]) == ("https://api.provable.com/v2", "k", "c")
+ marker = object()
+ assert Bridge.from_env(ethereum=None, solana=None).ethereum is None
+ assert Bridge.from_env(checkpoints=marker).checkpoints is marker
+ with pytest.raises(TypeError, match="unexpected"):
+ Bridge.from_env(w3=marker)
+
+
+def test_from_env_side_chain_variables(monkeypatch, tmp_path):
+ monkeypatch.setattr("aleo_bridge.client.build_aleo", lambda *a, **k: FakeAleo(mappings=default_mappings()))
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", "APrivateKey1zkpTest")
+ for var in ("EVM_PRIVATE_KEY", "ETHEREUM_RPC_URL", "BRIDGE_EVM_PRIVATE_KEY", "BRIDGE_LIVE_ETHEREUM_RPC_URL",
+ "SOLANA_PRIVATE_KEY", "SOLANA_RPC_URL", "BRIDGE_SOLANA_PRIVATE_KEY", "BRIDGE_LIVE_SOLANA_RPC_URL",
+ "BRIDGE_CHECKPOINT_DIR"):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("EVM_PRIVATE_KEY", "0x" + "11" * 32)
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Bridge.from_env()
+ monkeypatch.setenv("ETHEREUM_RPC_URL", "https://eth.example")
+ bridge = Bridge.from_env() # plan 2: real Ethereum connection now constructed
+ assert isinstance(bridge.ethereum, Ethereum)
+ assert bridge.ethereum.address == EthAccount.from_key("0x" + "11" * 32).address
+ monkeypatch.delenv("EVM_PRIVATE_KEY")
+ monkeypatch.delenv("ETHEREUM_RPC_URL")
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ from aleo_bridge._base58 import b58encode
+ from aleo_bridge.sol import Solana
+
+ solana_key = Keypair()
+ monkeypatch.setenv("SOLANA_PRIVATE_KEY", b58encode(bytes(solana_key)))
+ bridge = Bridge.from_env() # plan 3/4 (Task 4): real Solana connection now constructed
+ assert isinstance(bridge.solana, Solana)
+ assert bridge.solana.can_sign and bridge.solana.address == str(solana_key.pubkey())
+ monkeypatch.delenv("SOLANA_PRIVATE_KEY")
+ monkeypatch.setenv("BRIDGE_CHECKPOINT_DIR", str(tmp_path / "cp"))
+ bridge = Bridge.from_env() # plan 4: FileCheckpointStore now wired
+ from aleo_bridge.checkpoint import FileCheckpointStore
+ assert isinstance(bridge.checkpoints, FileCheckpointStore)
+ assert bridge.checkpoints.directory == tmp_path / "cp" and bridge.checkpoints.directory.is_dir()
+
+
+def test_build_aleo_is_local_only():
+ from aleo import testnet as net
+ from aleo_bridge.client import build_aleo
+ key = net.PrivateKey.random()
+ aleo = build_aleo("https://edge.provable.com/api", "testnet", str(key))
+ assert aleo.network_name == "testnet" and str(aleo.default_account.address) == str(key.address)
+
+
+def test_from_profile_uses_profile_and_wires_no_side_chains(tmp_path, monkeypatch):
+ captured = {}
+
+ def fake_build(endpoint, network, private_key, *, api_key=None, consumer_id=None):
+ captured.update(endpoint=endpoint, network=network, private_key=private_key)
+ return FakeAleo(mappings=default_mappings(), network_name=network)
+
+ monkeypatch.setattr("aleo_bridge.client.build_aleo", fake_build)
+ for var in ("BRIDGE_PRIVATE_KEY", "BRIDGE_PRIVATE_KEY_FILE", "EVM_PRIVATE_KEY", "ETHEREUM_RPC_URL",
+ "BRIDGE_EVM_PRIVATE_KEY", "BRIDGE_LIVE_ETHEREUM_RPC_URL", "SOLANA_PRIVATE_KEY",
+ "BRIDGE_SOLANA_PRIVATE_KEY", "BRIDGE_LIVE_SOLANA_RPC_URL", "ALEO_API_KEY", "ALEO_CONSUMER_ID"):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("ALEO_BRIDGE_HOME", str(tmp_path / "home"))
+ bridge = Bridge.from_profile(network="testnet", endpoint="https://api.provable.com/v2")
+ assert bridge.profile is not None and bridge.profile.home == tmp_path / "home"
+ assert captured == {"endpoint": "https://api.provable.com/v2", "network": "testnet", "private_key": bridge.profile.private_key}
+ assert bridge.environment == "testnet" and bridge.ethereum is None and bridge.solana is None
+ from aleo_bridge.checkpoint import FileCheckpointStore
+ assert isinstance(bridge.checkpoints, FileCheckpointStore) # plan 4 binds FileCheckpointStore(profile.checkpoint_dir)
+ assert bridge.checkpoints.directory == bridge.profile.checkpoint_dir
+ assert bridge.profile.checkpoint_dir.is_dir()
+ marker = Ethereum(w3=fake_web3())
+ assert Bridge.from_profile(ethereum=marker).ethereum is marker
+
+
+def test_cli_lists_routes_and_assets(capsys):
+ assert cli.main(["routes"]) == 0
+ routes = json.loads(capsys.readouterr().out)
+ assert len(routes) == 22 and routes[0] == "xreserve:ethereum/usdc->aleo/usdcx"
+ assert cli.main(["assets"]) == 0
+ assert len(json.loads(capsys.readouterr().out)) == 19
+ assert cli.main(["bogus"]) == 2
+
+
+def test_cli_prints_agent_guide_by_default(capsys):
+ import aleo_bridge
+
+ assert cli.main([]) == 0
+ out = capsys.readouterr().out
+ assert out == aleo_bridge.agent_guide()
+ assert "# aleo-bridge — agent guide" in out
diff --git a/bridge-sdk/tests/test_client_lifecycle.py b/bridge-sdk/tests/test_client_lifecycle.py
new file mode 100644
index 0000000..03882e1
--- /dev/null
+++ b/bridge-sdk/tests/test_client_lifecycle.py
@@ -0,0 +1,179 @@
+"""The Bridge methods are one-liners; test that each forwards every argument to
+lifecycle.* by calling the unbound methods with the FakeBridge as ``self``.
+
+Deviation from the task-9 brief (recorded in task-9-report.md): the brief's Step 3 also has
+``__init__.py`` import ``bridge_tools``/``dispatch_tool`` from a new ``.agent`` module and add
+``agent_guide()`` (reading a packaged ``AGENTS.md``), and has ``__main__.py`` print that guide.
+The task-9 controller notes (ruling 5) explicitly forbid creating ``agent.py``/``AGENTS.md`` in
+that task — those are Task 10/12's files. Task 10 landed ``agent.py`` and those three exports,
+so the export pin below now covers them; ``AGENTS.md`` is still Task 12's, and ``__main__.py``
+is left untouched.
+"""
+import inspect
+from pathlib import Path
+
+import aleo_bridge
+from aleo_bridge import agent, lifecycle
+from aleo_bridge.checkpoint import FileCheckpointStore, create_checkpoint
+from aleo_bridge.client import Bridge
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+
+# The full pre-existing __all__ (plans 1-3, before this task's additive edit) — pinned so this
+# task's edit can only ever ADD names, never drop one (controller ruling 1).
+PRE_EXISTING_EXPORTS = [
+ "__version__", "AmbiguousRouteError", "AttestationError", "BridgeError", "ChainMismatchError",
+ "CheckpointInvalidError", "ConfigurationError", "DeliveryUnknownError", "InsufficientBalanceError",
+ "InvalidAmountError", "InvalidRecipientError", "MissingExtraError", "NotResumableError",
+ "PollingTimeoutError", "RegistryVersionMismatchError", "RouteNotFoundError", "RouteUnavailableError",
+ "UnsupportedRouteError",
+ "Asset", "Chain", "DEFAULT_REGISTRY", "Locator", "Privacy", "Registry", "Route", "validate_registry",
+ "CALLER_BOUNDARIES", "TERMINAL", "AleoHyperlaneQuote", "AleoXReserveQuote", "Attestation", "BridgeStatus",
+ "BurnReceipt", "ChainStatus", "DepositReceipt", "DispatchReceipt", "EvmHyperlaneQuote", "EvmXReserveQuote",
+ "Fee", "GasQuote", "MintReceipt", "Plan", "PreparedTx", "PrivacyReceipt", "Progress", "Quote", "Receipt",
+ "SolanaHyperlaneQuote", "Status", "Step", "to_progress",
+ "AleoCall", "Bridge", "CircleClient", "DEFAULT_ENDPOINT", "EMPTY_MERKLE_PROOF_PAIR", "FreezeList",
+ "HyperlaneModule", "PrivacyModule", "Profile", "XReserveModule",
+ "Checkpoint", "CheckpointStore", "FileCheckpointStore", "create_checkpoint",
+ "EthModule", "Ethereum", "EvmCall",
+ "DEFAULT_SOLANA_RPC_URL", "Solana", "SolCall", "SolModule",
+]
+
+# This task's own additions (lifecycle module + the pure prepare() convenience import), then the
+# agent surface Task 9 deferred to Task 10 (bridge_tools/dispatch_tool + the packaged guide).
+NEW_EXPORTS = ["lifecycle", "prepare", "agent_guide", "bridge_tools", "dispatch_tool"]
+
+
+def _spy(monkeypatch, name):
+ seen = {}
+
+ def fake(bridge, *args, **kwargs):
+ seen["args"], seen["kwargs"], seen["bridge"] = args, kwargs, bridge
+ return "result"
+ monkeypatch.setattr(lifecycle, name, fake)
+ return seen
+
+
+def test_quote_forwards(monkeypatch):
+ seen = _spy(monkeypatch, "quote")
+ b = FakeBridge()
+ assert Bridge.quote(b, "ethereum/usdc", "aleo/usdcx", amount="2", recipient=ALEO_RECIPIENT,
+ mint_mode="private", secret_nonce="7scalar", sender=EVM_ADDRESS, protocol="xreserve") == "result"
+ assert seen["bridge"] is b and seen["args"] == ()
+ assert seen["kwargs"] == dict(source="ethereum/usdc", destination="aleo/usdcx", amount="2", amount_atomic=None,
+ recipient=ALEO_RECIPIENT, sender=EVM_ADDRESS, protocol="xreserve",
+ mint_mode="private", secret_nonce="7scalar")
+
+
+def test_execute_wait_get_status_recover_resume_complete_forward(monkeypatch):
+ b = FakeBridge()
+ seen = _spy(monkeypatch, "execute")
+ cb = lambda cp: None
+ Bridge.execute(b, "PLAN", on_checkpoint=cb, proving="local", mode="signer", record="r", merkle_proof="m",
+ gas_payment_microcredits=5, secret_nonce="1scalar", poll_seconds=2.0, timeout_seconds=3.0)
+ assert seen["args"] == ("PLAN",) and seen["kwargs"] == dict(
+ on_checkpoint=cb, proving="local", mode="signer", record="r", merkle_proof="m", gas_payment_microcredits=5,
+ secret_nonce="1scalar", poll_seconds=2.0, timeout_seconds=3.0)
+ seen = _spy(monkeypatch, "wait")
+ on_err = lambda exc: None
+ Bridge.wait(b, "PROGRESS", until=[Status.DELIVERY_PENDING], poll_seconds=1, timeout_seconds=2, on_update=cb,
+ on_error=on_err, max_consecutive_errors=9)
+ assert seen["args"] == ("PROGRESS",) and seen["kwargs"] == dict(until=[Status.DELIVERY_PENDING], poll_seconds=1,
+ timeout_seconds=2, on_update=cb, on_error=on_err,
+ max_consecutive_errors=9)
+ seen = _spy(monkeypatch, "get_status")
+ Bridge.get_status(b, "PLAN", "RECEIPT")
+ assert seen["args"] == ("PLAN", "RECEIPT")
+ seen = _spy(monkeypatch, "recover")
+ Bridge.recover(b, {"version": 1})
+ assert seen["args"] == ({"version": 1},)
+ seen = _spy(monkeypatch, "resume")
+ Bridge.resume(b, "PROGRESS", on_checkpoint=cb, secret_nonce="1scalar", poll_seconds=1.0, timeout_seconds=9.0)
+ assert seen["kwargs"] == dict(on_checkpoint=cb, secret_nonce="1scalar", poll_seconds=1.0, timeout_seconds=9.0,
+ proving="delegate")
+ seen = _spy(monkeypatch, "complete")
+ Bridge.complete(b, "PROGRESS", secret_nonce="7scalar", on_checkpoint=cb)
+ assert seen["kwargs"] == dict(secret_nonce="7scalar", on_checkpoint=cb, proving="delegate")
+
+
+class _Boom:
+ """Any attribute access returns a callable that raises — stands in for ``eth``/``sol`` so a
+ test can assert ``pending()`` never touches the network, not merely that it happened to answer
+ correctly this time."""
+
+ def __getattr__(self, name):
+ def raiser(*args, **kwargs):
+ raise AssertionError(f"Bridge.pending() must not touch the network (called .{name})")
+ return raiser
+
+
+def test_pending_recovers_every_stored_checkpoint_offline(tmp_path):
+ # Fix round 1 (F1): pending() must not touch the network — one unreachable chain must never
+ # hide every other in-flight transfer. An EVM and a Solana checkpoint in the same store, with
+ # both side-chain modules wired to raise on ANY call, still both come back.
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(solana=True, checkpoints=store)
+ b._eth, b._sol = _Boom(), _Boom()
+
+ evm_plan = lifecycle.prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001",
+ recipient=ALEO_RECIPIENT, sender=EVM_ADDRESS)
+ store.save(create_checkpoint(evm_plan, Receipt(id="0x" + "11" * 32, protocol="hyperlane",
+ status=Status.SOURCE_CONFIRMING, source_tx_id="0x" + "11" * 32,
+ protocol_state={"routeId": evm_plan.route_id}), b.registry))
+ sol_plan = lifecycle.prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ store.save(create_checkpoint(sol_plan, Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="sig", protocol_state={"routeId": sol_plan.route_id}),
+ b.registry))
+
+ out = Bridge.pending(b)
+ assert len(out) == 2 and all(p.next == "wait" for p in out) and b.calls == []
+ assert {p.receipt.source_tx_id for p in out} == {"0x" + "11" * 32, "sig"}
+ assert Bridge.pending(FakeBridge(ethereum=False)) == []
+
+
+def test_pending_folds_a_malformed_checkpoint_into_a_failed_entry(tmp_path):
+ # F1: a checkpoint that cannot be interpreted (well-formed JSON/route, but nothing to build a
+ # receipt from — e.g. a truncated file that lost its "source" block) must not hide the
+ # healthy checkpoints alongside it in the same store.
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ plan = lifecycle.prepare(b.registry, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=EVM_ADDRESS)
+ store.save(create_checkpoint(plan, Receipt(id="at1good", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="at1good", protocol_state={"routeId": plan.route_id}),
+ b.registry))
+ store.save(create_checkpoint(plan, Receipt(id="at1ghost", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": plan.route_id}), b.registry)) # no source tx at all
+
+ out = Bridge.pending(b)
+ assert len(out) == 2
+ good, bad = out
+ assert good.next == "wait" and good.receipt.source_tx_id == "at1good"
+ assert bad.next == "failed" and bad.error is not None and "no submitted source transaction" in bad.error
+
+
+def test_bridge_verb_signatures_are_a_superset_of_the_lifecycle_verb_they_forward_to():
+ # F2 guard: a future edit that drops a kwarg from a Bridge verb (without dropping it from the
+ # matching lifecycle verb too) fails loudly here instead of silently losing a caller option.
+ for name in ("quote", "execute", "get_status", "wait", "recover", "resume", "complete"):
+ bridge_params = set(inspect.signature(getattr(Bridge, name)).parameters)
+ lifecycle_params = set(inspect.signature(getattr(lifecycle, name)).parameters) - {"bridge"}
+ assert lifecycle_params <= bridge_params, name
+
+
+def test_public_exports_pin_pre_existing_set_and_add_lifecycle_names():
+ for name in PRE_EXISTING_EXPORTS + NEW_EXPORTS:
+ 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.1.0"
+ assert aleo_bridge.lifecycle is lifecycle
+ assert aleo_bridge.prepare is lifecycle.prepare
+ assert aleo_bridge.bridge_tools is agent.bridge_tools
+ assert aleo_bridge.dispatch_tool is agent.dispatch_tool
+ # AGENTS.md is Task 12's generated file: until it ships, the guide is a pointer, never an error
+ guide = aleo_bridge.agent_guide()
+ assert isinstance(guide, str) and guide
+ if not (Path(aleo_bridge.__file__).with_name("AGENTS.md")).exists():
+ assert "codegen/gen_context.py" in guide
diff --git a/bridge-sdk/tests/test_encoding.py b/bridge-sdk/tests/test_encoding.py
new file mode 100644
index 0000000..0940f8a
--- /dev/null
+++ b/bridge-sdk/tests/test_encoding.py
@@ -0,0 +1,214 @@
+import pytest
+
+from aleo_bridge import encoding as enc
+from aleo_bridge.errors import AttestationError, ConfigurationError, InvalidAmountError, InvalidRecipientError
+
+RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+RECIPIENT_BYTES32 = "b102e0d37e02ec5dbba2460287ac07ef7ea8ee636392ce235402308299901811"
+EVM1 = "0x0000000000000000000000000000000000000001"
+WRAPPER = "shielded_usdcx_wrapper.aleo"
+MESSAGE_ID = "0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d"
+
+
+# ── bech32m (veil test/utils/xreserve.test.ts) ────────────────────────────────
+
+def test_aleo_address_round_trip_and_checksum():
+ assert enc.aleo_address_to_bytes32(RECIPIENT).hex() == RECIPIENT_BYTES32
+ assert enc.bytes32_to_aleo_address(bytes.fromhex(RECIPIENT_BYTES32)) == RECIPIENT
+ with pytest.raises(InvalidRecipientError, match="Invalid Aleo recipient"):
+ enc.aleo_address_to_bytes32(RECIPIENT[:-1] + "q")
+ with pytest.raises(InvalidRecipientError):
+ enc.aleo_address_to_bytes32("aleo1short")
+ with pytest.raises(InvalidRecipientError):
+ enc.aleo_address_to_bytes32("aleo1" + "b" * 58) # 'b' is not in the alphabet
+ with pytest.raises(InvalidRecipientError, match="32-byte Aleo recipient"):
+ enc.bytes32_to_aleo_address(b"\x01")
+
+
+def test_bech32_decoder_agrees_with_the_bindings():
+ # Address.from_string(...).to_bits_le() is a 253-bit field-element encoding (no fixed byte
+ # width) and Plaintext.from_string(...).to_bits_le() prefixes a literal-type discriminant
+ # (279 bits total here) — neither packs cleanly into 32 bytes. to_bytes_le() is the API that
+ # actually returns the 32-byte payload, so that's what the bech32m payload is checked against.
+ from aleo import mainnet as net
+ packed = bytes(net.Address.from_string(RECIPIENT).to_bytes_le())
+ assert len(packed) == 32
+ assert enc.aleo_address_to_bytes32(RECIPIENT) == packed
+
+
+# ── EVM addresses ─────────────────────────────────────────────────────────────
+
+def test_evm_address_to_bytes32_left_pads_and_checks_eip55():
+ assert enc.evm_address_to_bytes32(EVM1).hex() == "00" * 31 + "01"
+ assert enc.evm_address_to_bytes32("0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238").hex() == \
+ "0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238"
+ assert enc.is_evm_address("0x1c7d4b196cb0c7b01d743fbc6116a902379c7238") # all-lowercase is fine
+ assert enc.to_checksum_address("0x1c7d4b196cb0c7b01d743fbc6116a902379c7238") == \
+ "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
+ with pytest.raises(InvalidRecipientError, match="Invalid Ethereum recipient"):
+ enc.evm_address_to_bytes32("0x1234")
+ with pytest.raises(InvalidRecipientError): # mixed case with a wrong checksum
+ enc.evm_address_to_bytes32("0x1C7D4B196Cb0C7B01d743Fbc6116a902379C7238")
+
+
+# ── Hyperlane limbs (veil test/utils/hyperlane.test.ts) ───────────────────────
+
+def test_evm_limbs():
+ assert enc.evm_address_to_hyperlane_recipient("0x1e196d0a7d8189054c4db744ab3340c3f1c68b19") == (
+ 13858749752514421660238621190289096704, 33956464229475118999063216025592496509)
+ assert enc.evm_address_to_hyperlane_recipient(EVM1) == (0, 1329227995784915872903807060280344576)
+ for bad in ("0x1234", "0x" + "11" * 32):
+ with pytest.raises(InvalidRecipientError, match="Invalid Ethereum Hyperlane recipient"):
+ enc.evm_address_to_hyperlane_recipient(bad)
+
+
+def test_solana_limbs():
+ assert enc.solana_address_to_hyperlane_recipient("8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7") == (
+ 127878782877948140186055645953777992816, 163261512394675613100746600600636171918)
+ assert enc.solana_address_to_hyperlane_recipient("11111111111111111111111111111111") == (0, 0)
+ for bad in ("not-base58!", "1111"):
+ with pytest.raises(InvalidRecipientError, match="Invalid Solana Hyperlane recipient"):
+ enc.solana_address_to_hyperlane_recipient(bad)
+
+
+def test_limbs_and_literals():
+ assert enc.bytes32_to_u128_limbs(bytes(31) + b"\x01") == (0, 1 << 120)
+ with pytest.raises(InvalidRecipientError):
+ enc.bytes32_to_u128_limbs(bytes(31))
+ assert enc.u128_pair_literal((0, 1329227995784915872903807060280344576)) == \
+ "[0u128, 1329227995784915872903807060280344576u128]"
+ assert enc.u8_array_literal(bytes.fromhex("00ff")) == "[0u8,255u8]"
+ assert enc.u8_array_literal(bytes(31) + b"\x01") == "[" + ",".join(["0u8"] * 31 + ["1u8"]) + "]"
+ assert enc.hex_to_bytes("0x00ff", 2) == b"\x00\xff"
+ assert enc.hex_to_bytes(b"\x00\xff") == b"\x00\xff"
+ with pytest.raises(InvalidRecipientError, match="32 bytes"):
+ enc.hex_to_bytes("0x00ff", 32)
+ assert enc.to_hex(b"\x00\xff") == "0x00ff"
+
+
+def test_hyperlane_delivery_key_vector():
+ # brief §3.7 vector
+ key = enc.hyperlane_delivery_key(enc.hex_to_bytes(MESSAGE_ID, 32))
+ assert key == "{ id: [262854447642257427123071959211115528903u128, 102980212169860384794748804418278302317u128] }"
+ with pytest.raises(InvalidRecipientError):
+ enc.hyperlane_delivery_key(b"\x00")
+
+
+# ── xReserve wire format (veil test/utils/xreserve.test.ts) ───────────────────
+
+def _payload(hook: bytes) -> tuple[bytes, bytes]:
+ nonce = enc.xreserve_deposit_nonce(0, bytes.fromhex("12" * 32), 4)
+ payload = enc.xreserve_deposit_payload(
+ amount=1_000_000, remote_domain=10002,
+ remote_token=bytes.fromhex("b143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401"),
+ remote_recipient=enc.aleo_address_to_bytes32(RECIPIENT),
+ local_token="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", depositor=EVM1,
+ max_fee=100_000, nonce=nonce, hook_data=hook)
+ return nonce, payload
+
+
+def test_deposit_nonce_payload_and_hash_layout():
+ nonce, payload = _payload(enc.xreserve_hook_data("public", RECIPIENT, "testnet"))
+ assert len(nonce) == 32 and len(payload) == 305
+ assert payload[0:8].hex() == "5a2e0acd00000001"
+ assert payload[8:40] == (1_000_000).to_bytes(32, "big")
+ assert payload[40:44] == (10002).to_bytes(4, "big")
+ assert payload[44:76].hex() == "b143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401"
+ assert payload[76:108].hex() == RECIPIENT_BYTES32
+ assert payload[108:140].hex() == "0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c7238"
+ assert payload[140:172].hex() == "00" * 31 + "01"
+ assert payload[172:204] == (100_000).to_bytes(32, "big")
+ assert payload[204:236] == nonce
+ assert payload[236:240].hex() == "00000041"
+ assert payload[240:305] == bytes(65)
+ assert len(enc.xreserve_message_hash(payload)) == 32
+ assert enc.xreserve_nonce_from_payload(payload) == nonce
+
+
+def test_deposit_nonce_matches_abi_encoding_via_web3():
+ web3 = pytest.importorskip("web3")
+ from eth_abi import encode
+ tx_hash = bytes.fromhex("12" * 32)
+ expected = bytes(web3.Web3.keccak(encode(["uint32"], [0]) + tx_hash + encode(["uint256"], [4])))
+ assert enc.xreserve_deposit_nonce(0, tx_hash, 4) == expected
+ assert enc.xreserve_deposit_nonce(0, "0x" + "12" * 32, 4) == expected # hex accepted too
+
+
+def test_deposit_payload_rejects_bad_widths():
+ good = dict(amount=1, remote_domain=1, remote_token=bytes(32), remote_recipient=bytes(32),
+ local_token=EVM1, depositor=EVM1, max_fee=0, nonce=bytes(32), hook_data=bytes(65))
+ with pytest.raises(InvalidRecipientError, match="remote_token must contain 32 bytes"):
+ enc.xreserve_deposit_payload(**{**good, "remote_token": bytes(31)})
+ with pytest.raises(InvalidRecipientError, match="hook_data must contain 65 bytes"):
+ enc.xreserve_deposit_payload(**{**good, "hook_data": bytes(64)})
+ with pytest.raises(InvalidAmountError, match="does not fit"):
+ enc.xreserve_deposit_payload(**{**good, "remote_domain": 1 << 32})
+ with pytest.raises(InvalidRecipientError):
+ enc.xreserve_deposit_payload(**{**good, "depositor": "0x1234"})
+
+
+def test_deposit_nonce_bounds_source_domain_to_uint32():
+ tx_hash = bytes.fromhex("12" * 32)
+ assert enc.xreserve_deposit_nonce((1 << 32) - 1, tx_hash, 0) # max uint32 is fine
+ with pytest.raises(InvalidAmountError, match="does not fit"):
+ enc.xreserve_deposit_nonce(1 << 32, tx_hash, 0)
+
+
+def test_nonce_from_payload_rejects_bad_layout():
+ with pytest.raises(AttestationError, match="invalid deposit layout"):
+ enc.xreserve_nonce_from_payload(bytes(305))
+ with pytest.raises(AttestationError):
+ enc.xreserve_nonce_from_payload(bytes(304))
+
+
+# ── hook data (BHP256 through the real bindings) ──────────────────────────────
+
+def test_hook_data_public_and_record_are_pure():
+ assert enc.xreserve_hook_data("public", RECIPIENT, "testnet") == bytes(65)
+ assert enc.xreserve_hook_data("record", RECIPIENT, "testnet") == b"\x01" + bytes(64)
+
+
+def test_hook_data_private_commits_recipient_with_selected_scalar():
+ default = enc.xreserve_hook_data("private", RECIPIENT, "testnet")
+ assert len(default) == 65 and default[0] == 2 and default[33:] == bytes(32)
+ assert default == enc.xreserve_hook_data("private", RECIPIENT, "testnet", "0scalar")
+ custom = enc.xreserve_hook_data("private", RECIPIENT, "testnet", "7scalar")
+ assert custom[0] == 2 and custom != default
+ # BHP256 is curve-level, not network-level: mainnet and testnet bindings agree.
+ assert enc.xreserve_hook_data("private", RECIPIENT, "mainnet") == default
+ # Independent recomputation through the bindings.
+ from aleo import testnet as net
+ commitment = net.BHP256().commit(net.Plaintext.from_string(RECIPIENT).to_bits_le(),
+ net.Scalar.from_string("7scalar"))
+ assert custom[1:33] == bytes(commitment.to_bytes_le())
+
+
+def test_hook_data_private_pinned_vector():
+ # Pinned from the bindings on 2026-09-03 (RECIPIENT, 0scalar). If this fails while the previous
+ # test passes, the bindings' BHP256 output changed — investigate, do not re-pin blindly.
+ assert enc.xreserve_hook_data("private", RECIPIENT, "mainnet")[1:33].hex() == \
+ "dd46b467d619a9628e58a71ebf24873c777f93ae2b7ac5dee3db7282ecef8d10"
+
+
+def test_hook_data_validation():
+ with pytest.raises(ConfigurationError, match="mint mode"):
+ enc.xreserve_hook_data("shielded", RECIPIENT, "mainnet")
+ with pytest.raises(ConfigurationError, match="scalar"):
+ enc.xreserve_hook_data("private", RECIPIENT, "mainnet", "not-a-scalar")
+ with pytest.raises(ConfigurationError, match="network"):
+ enc.xreserve_hook_data("private", RECIPIENT, "devnet")
+ with pytest.raises(InvalidRecipientError):
+ enc.xreserve_hook_data("private", "aleo1short", "mainnet")
+
+
+# ── program address ───────────────────────────────────────────────────────────
+
+def test_program_address_is_a_valid_address_and_matches_bindings():
+ from aleo import mainnet as net
+ addr = enc.aleo_program_address(WRAPPER, "mainnet")
+ assert addr == str(net.Address.from_program_id(WRAPPER))
+ assert len(enc.aleo_address_to_bytes32(addr)) == 32
+ # Pinned 2026-09-03 from the bindings; see note on the hook-data pin above.
+ assert addr == "aleo183r3zgsr57fwtgk5duzeq9kqdkpmmtfj4k5469ddvm3tcfhhls9szktw82"
+ with pytest.raises(ConfigurationError, match="network"):
+ enc.aleo_program_address(WRAPPER, "devnet")
diff --git a/bridge-sdk/tests/test_eth_connection.py b/bridge-sdk/tests/test_eth_connection.py
new file mode 100644
index 0000000..8a8063f
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_connection.py
@@ -0,0 +1,203 @@
+import pytest
+from eth_account import Account
+from web3 import Web3
+from web3.middleware import SignAndSendRawMiddlewareBuilder
+
+from aleo_bridge.errors import ConfigurationError
+from tests.fakes.fake_web3 import fake_web3
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+TO = "0x0000000000000000000000000000000000000002"
+
+
+def test_exactly_one_transport():
+ from aleo_bridge.eth import Ethereum
+
+ with pytest.raises(ConfigurationError, match="exactly one of rpc_url or w3"):
+ Ethereum()
+ with pytest.raises(ConfigurationError, match="exactly one of rpc_url or w3"):
+ Ethereum("http://localhost:8545", w3=fake_web3())
+
+
+def test_at_most_one_signer():
+ from aleo_bridge.eth import Ethereum
+
+ with pytest.raises(ConfigurationError, match="at most one of signer or private_key"):
+ Ethereum(w3=fake_web3(), signer=ACCT, private_key=KEY)
+
+
+def test_rpc_url_builds_http_provider_lazily():
+ from web3 import HTTPProvider
+
+ from aleo_bridge.eth import Ethereum
+
+ conn = Ethereum("http://127.0.0.1:1", private_key=KEY)
+ assert isinstance(conn.w3.provider, HTTPProvider)
+ assert conn.address == ACCT.address and conn.can_sign
+
+
+def test_w3_plus_signer_form_and_cached_chain_id():
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3(chain_id=11155111)
+ conn = Ethereum(w3=w3, signer=ACCT)
+ assert conn.w3 is w3 and conn.address == ACCT.address and conn.can_sign
+ assert conn.chain_id == 11155111
+ assert conn.chain_id == 11155111 and w3.provider.methods.count("eth_chainId") == 1
+
+
+def test_w3_alone_is_read_only_without_default_account():
+ from aleo_bridge.eth import Ethereum
+
+ conn = Ethereum(w3=fake_web3())
+ assert conn.address is None and not conn.can_sign
+ with pytest.raises(ConfigurationError, match="read-only"):
+ conn.send_transaction({"to": TO, "value": 1, "data": "0x"})
+ with pytest.raises(ConfigurationError, match="read-only"):
+ conn.require_address()
+
+
+def test_w3_default_account_uses_callers_middleware():
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3()
+ w3.middleware_onion.inject(SignAndSendRawMiddlewareBuilder.build(ACCT), layer=0)
+ w3.eth.default_account = ACCT.address
+ conn = Ethereum(w3=w3)
+ assert conn.address == ACCT.address and conn.can_sign
+ h = conn.send_transaction({"to": TO, "value": 1, "data": "0x"})
+ assert h == w3.provider.hash_at(1)
+ assert "eth_sendRawTransaction" in w3.provider.methods # the caller's middleware signed
+ assert w3.provider.sent[0]["from"] == ACCT.address and w3.provider.sent[0]["value"] == 1
+
+
+def test_local_account_path_signs_and_sends_raw():
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3()
+ conn = Ethereum(w3=w3, private_key=KEY)
+ h = conn.send_transaction({"to": TO, "value": 7, "data": "0x"})
+ assert h == w3.provider.hash_at(1)
+ assert "eth_sendRawTransaction" in w3.provider.methods and "eth_sendTransaction" not in w3.provider.methods
+ sent = w3.provider.sent[0]
+ assert sent["from"] == ACCT.address and sent["to"] == Web3.to_checksum_address(TO) and sent["value"] == 7
+ # eth.py's fee-filling (EIP-1559 path, since the fake's eth_getBlockByNumber carries baseFeePerGas):
+ # nonce <- eth_getTransactionCount(sender, "pending")
+ # gas <- eth_estimateGas(...) * 12 // 10 (a 20% buffer)
+ # maxPriorityFeePerGas <- eth_maxPriorityFeePerGas
+ # maxFeePerGas <- baseFeePerGas * 2 + maxPriorityFeePerGas
+ assert sent["nonce"] == 0
+ assert sent["gas"] == 150_000 * 12 // 10
+ assert sent["maxPriorityFeePerGas"] == 10**8
+ assert sent["maxFeePerGas"] == 10**9 * 2 + 10**8
+ assert "gasPrice" not in sent
+
+
+def test_legacy_gas_price_path_when_no_base_fee():
+ """With no ``baseFeePerGas`` on the latest block (pre-EIP-1559 chain), eth.py falls back
+ to a plain ``gasPrice`` from ``eth_gasPrice`` and sets no 1559 fee fields."""
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3(legacy=True)
+ conn = Ethereum(w3=w3, private_key=KEY)
+ h = conn.send_transaction({"to": TO, "value": 3, "data": "0x"})
+ assert h == w3.provider.hash_at(1)
+ sent = w3.provider.sent[0]
+ assert sent["gasPrice"] == 10**9
+ assert "maxFeePerGas" not in sent and "maxPriorityFeePerGas" not in sent
+ assert "eth_maxPriorityFeePerGas" not in w3.provider.methods
+
+
+def test_sender_mismatch_is_refused():
+ from aleo_bridge.eth import Ethereum
+
+ conn = Ethereum(w3=fake_web3(), private_key=KEY)
+ with pytest.raises(ConfigurationError, match="does not match the configured account"):
+ conn.send_transaction({"from": TO, "to": TO, "value": 0, "data": "0x"})
+
+
+def test_a_lost_send_response_names_the_locally_computed_hash():
+ """``eth_sendRawTransaction`` failing tells us nothing about whether the node took the bytes:
+ the hash is already determined by the signature, so it must reach the caller."""
+ from aleo_bridge.errors import BridgeError
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3()
+ w3.provider.send_errors[1] = "connection reset by peer"
+ conn = Ethereum(w3=w3, private_key=KEY)
+ signed = ACCT.sign_transaction({"to": Web3.to_checksum_address(TO), "value": 5, "data": "0x", "chainId": 1,
+ "nonce": 0, "gas": 150_000 * 12 // 10, "maxPriorityFeePerGas": 10**8,
+ "maxFeePerGas": 10**9 * 2 + 10**8})
+ with pytest.raises(BridgeError) as exc:
+ conn.send_transaction({"to": TO, "value": 5, "data": "0x"})
+ message = str(exc.value)
+ assert Web3.to_hex(signed.hash) in message and "may have been broadcast" in message
+ assert "connection reset by peer" in message
+ assert w3.provider.methods.count("eth_sendRawTransaction") == 1 # exactly one attempt
+
+
+def test_a_node_hash_that_differs_from_the_signed_one_is_refused():
+ from aleo_bridge.errors import BridgeError
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3()
+ other = "0x" + "ab" * 32
+ w3.provider.echo_hashes[1] = other
+ conn = Ethereum(w3=w3, private_key=KEY)
+ with pytest.raises(BridgeError) as exc:
+ conn.send_transaction({"to": TO, "value": 5, "data": "0x"})
+ message = str(exc.value)
+ assert other in message and w3.provider.hash_at(1) in message
+
+
+def test_wait_for_receipt_returns_none_on_timeout_and_dict_on_success():
+ from aleo_bridge.eth import Ethereum
+
+ w3 = fake_web3()
+ conn = Ethereum(w3=w3, private_key=KEY)
+ h = conn.send_transaction({"to": TO, "value": 0, "data": "0x"})
+ w3.provider.pending.add(h)
+ assert conn.wait_for_receipt(h, timeout_seconds=0.01, poll_seconds=0.001) is None
+ assert conn.get_receipt(h) is None
+ w3.provider.pending.clear()
+ receipt = conn.wait_for_receipt(h, timeout_seconds=1.0, poll_seconds=0.001)
+ assert receipt is not None and int(receipt["status"]) == 1 and int(receipt["blockNumber"]) == 0x11
+ assert Web3.to_hex(conn.get_receipt(h)["transactionHash"]) == h
+
+
+def test_get_receipt_missing_hash_is_none():
+ from aleo_bridge.eth import Ethereum
+
+ conn = Ethereum(w3=fake_web3())
+ assert conn.get_receipt("0x" + "99" * 32) is None
+
+
+def test_from_env():
+ from aleo_bridge.eth import Ethereum
+
+ assert Ethereum.from_env({}) is None
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Ethereum.from_env({"EVM_PRIVATE_KEY": KEY})
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Ethereum.from_env({"ETHEREUM_RPC_URL": "http://127.0.0.1:1"})
+ conn = Ethereum.from_env({"EVM_PRIVATE_KEY": KEY, "ETHEREUM_RPC_URL": "http://127.0.0.1:1"})
+ assert conn is not None and conn.address == ACCT.address and conn.w3.provider.endpoint_uri == "http://127.0.0.1:1"
+
+
+def test_from_env_aliases(monkeypatch):
+ """``BRIDGE_EVM_PRIVATE_KEY`` / ``BRIDGE_LIVE_ETHEREUM_RPC_URL`` stand in for the primary variables."""
+ from aleo_bridge.eth import Ethereum
+
+ for var in ("EVM_PRIVATE_KEY", "ETHEREUM_RPC_URL", "BRIDGE_EVM_PRIVATE_KEY", "BRIDGE_LIVE_ETHEREUM_RPC_URL"):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("BRIDGE_EVM_PRIVATE_KEY", KEY)
+ with pytest.raises(ConfigurationError, match="both EVM_PRIVATE_KEY and ETHEREUM_RPC_URL"):
+ Ethereum.from_env()
+ monkeypatch.setenv("BRIDGE_LIVE_ETHEREUM_RPC_URL", "http://127.0.0.1:1")
+ conn = Ethereum.from_env()
+ assert conn is not None and conn.address == ACCT.address and conn.w3.provider.endpoint_uri == "http://127.0.0.1:1"
+ # the primary variable wins when both a primary and its alias are set
+ monkeypatch.setenv("ETHEREUM_RPC_URL", "http://127.0.0.1:2")
+ conn2 = Ethereum.from_env()
+ assert conn2.w3.provider.endpoint_uri == "http://127.0.0.1:2"
diff --git a/bridge-sdk/tests/test_eth_hyperlane_execute.py b/bridge-sdk/tests/test_eth_hyperlane_execute.py
new file mode 100644
index 0000000..99f721f
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_hyperlane_execute.py
@@ -0,0 +1,232 @@
+import dataclasses
+
+import pytest
+from eth_account import Account
+from eth_utils import keccak
+from web3 import Web3
+
+from aleo_bridge.errors import (BridgeError, ConfigurationError, InvalidRecipientError,
+ RegistryVersionMismatchError, RouteUnavailableError)
+from aleo_bridge.eth import Ethereum
+from aleo_bridge.types import DispatchReceipt, Status
+from tests.fakes.fake_web3 import ZERO_ADDRESS, dispatch_id_log, event_log, fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+OTHER_ALEO = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px"
+OTHER = "0x0000000000000000000000000000000000000009"
+ALEO_BYTES32 = "0xb102e0d37e02ec5dbba2460287ac07ef7ea8ee636392ce235402308299901811"
+ETH_ROUTER = "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A"
+WBTC, WBTC_ROUTER = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "0x20CDC85778b732073F7EecEF3DF25c0d310f8772"
+USDT, USDT_ROUTER = "0xdAC17F958D2ee523a2206206994597C13D831ec7", "0x3C2064D78e4578E8F936E3db42aEF044E33FBF31"
+MAILBOX = "0xc005dc82818d67AF737725bD4bf75435d065D239"
+MESSAGE_ID = bytes.fromhex("ab" * 32)
+APPROVE = keccak(text="approve(address,uint256)")[:4].hex()
+TRANSFER_REMOTE = keccak(text="transferRemote(uint32,bytes32,uint256)")[:4].hex()
+
+
+def setup(router, *, with_dispatch_log=True, **config):
+ w3 = fake_web3(**config)
+ if with_dispatch_log:
+ w3.provider.receipt_logs = lambda tx: (
+ [dispatch_id_log(MAILBOX, MESSAGE_ID, tx_hash=tx["hash"])] if tx["to"] == Web3.to_checksum_address(router) else [])
+ bridge = make_bridge(ethereum=Ethereum(w3=w3, private_key=KEY))
+ return bridge.eth, w3
+
+
+def test_native_eth_dispatch_is_one_transaction_with_value():
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 69_000_000_000_101)]})
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert isinstance(result, DispatchReceipt)
+ assert len(w3.provider.sent) == 1 and w3.provider.sent[0]["value"] == 0x3EC1507D5065
+ assert w3.provider.sent[0]["to"] == Web3.to_checksum_address(ETH_ROUTER)
+ assert w3.provider.sent[0]["data"][2:10] == TRANSFER_REMOTE
+ receipt = result.receipt
+ assert receipt.status == Status.DELIVERY_PENDING and receipt.protocol == "hyperlane"
+ assert result.message_id == Web3.to_hex(MESSAGE_ID) and receipt.id == result.message_id
+ assert receipt.source_tx_id == w3.provider.hash_at(1) == result.transaction_id and result.amount_atomic == 100
+ assert receipt.protocol_state == {
+ "routeId": "hyperlane:ethereum/eth->aleo/eth", "approvalTxIds": [], "sourceSender": ACCT.address,
+ "recipientBytes32": ALEO_BYTES32, "destinationDomain": 1634493807,
+ "nativeValueAtomic": "69000000000101", "amountAtomic": "100", "messageId": Web3.to_hex(MESSAGE_ID),
+ }
+
+
+def test_wbtc_approves_exact_token_amount_then_dispatches_with_fee_value():
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]})
+ result = eth.transfer_remote("ethereum/wbtc", ALEO, amount="0.001").send(poll_seconds=0.001)
+ sent = w3.provider.sent
+ assert [t["to"] for t in sent] == [Web3.to_checksum_address(WBTC), Web3.to_checksum_address(WBTC_ROUTER)]
+ assert sent[0]["data"][2:].lower() == APPROVE + WBTC_ROUTER[2:].lower().rjust(64, "0") + format(100_000, "064x")
+ assert sent[0]["value"] == 0 and sent[1]["value"] == 0xC350
+ assert result.receipt.protocol_state["approvalTxIds"] == [w3.provider.hash_at(1)] and result.receipt.source_tx_id == w3.provider.hash_at(2)
+
+
+def test_wbtc_sufficient_allowance_skips_approval():
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]},
+ allowances={(WBTC, ACCT.address, WBTC_ROUTER): 100_000})
+ result = eth.transfer_remote("wbtc", ALEO, amount_atomic=100_000).send(poll_seconds=0.001)
+ assert len(w3.provider.sent) == 1 and result.receipt.protocol_state["approvalTxIds"] == []
+
+
+def test_usdt_resets_non_zero_allowance_first():
+ eth, w3 = setup(USDT_ROUTER, quotes={USDT_ROUTER: [(ZERO_ADDRESS, 50_000), (USDT, 1_000_000)]},
+ allowances={(USDT, ACCT.address, USDT_ROUTER): 1})
+ result = eth.transfer_remote("usdt", ALEO, amount="1").send(poll_seconds=0.001)
+ sent = w3.provider.sent
+ assert len(sent) == 3
+ assert sent[0]["data"][2:].lower() == APPROVE + USDT_ROUTER[2:].lower().rjust(64, "0") + "0" * 64
+ assert sent[1]["data"][2:].lower() == APPROVE + USDT_ROUTER[2:].lower().rjust(64, "0") + format(1_000_000, "064x")
+ assert sent[2]["data"][2:10] == TRANSFER_REMOTE
+ assert result.receipt.protocol_state["approvalTxIds"] == [w3.provider.hash_at(1), w3.provider.hash_at(2)]
+
+
+def test_usdt_zero_allowance_needs_no_reset():
+ eth, w3 = setup(USDT_ROUTER, quotes={USDT_ROUTER: [(ZERO_ADDRESS, 50_000), (USDT, 1_000_000)]})
+ eth.transfer_remote("usdt", ALEO, amount="1").send(poll_seconds=0.001)
+ assert len(w3.provider.sent) == 2
+
+
+def test_approval_timeout_is_pending_and_checkpointed_before_polling():
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]})
+ w3.provider.pending_nth.add(1)
+ seen = []
+ result = eth.transfer_remote("wbtc", ALEO, amount_atomic=100_000).send(
+ timeout_seconds=0.01, poll_seconds=0.001, on_checkpoint=seen.append)
+ assert result.receipt.status == Status.SOURCE_APPROVAL_PENDING and result.receipt.source_tx_id is None
+ assert result.receipt.id == w3.provider.hash_at(1) and result.message_id is None and len(w3.provider.sent) == 1
+ assert [cp.source for cp in seen] == [{"approvalTransactionIds": [w3.provider.hash_at(1)]}]
+ assert seen[0].intent == {"source": {"chain": "ethereum", "asset": "wbtc"}, "destination": {"chain": "aleo", "asset": "wbtc"},
+ "bridgeProtocol": "hyperlane", "amount": "0.001", "recipient": ALEO,
+ "sender": ACCT.address, "mintMode": "public"}
+
+
+def test_dispatch_timeout_is_source_confirming_with_hash():
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ w3.provider.pending_nth.add(1)
+ seen = []
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(
+ timeout_seconds=0.01, poll_seconds=0.001, on_checkpoint=seen.append)
+ assert result.receipt.status == Status.SOURCE_CONFIRMING and result.receipt.source_tx_id == w3.provider.hash_at(1)
+ assert result.receipt.id == w3.provider.hash_at(1) and "messageId" not in result.receipt.protocol_state
+ assert [cp.source for cp in seen] == [{"transactionId": w3.provider.hash_at(1)}]
+
+
+def test_dispatch_id_survives_unrelated_log_before_it():
+ """A log from an unrelated event (different address/topic, e.g. an ERC-20 Transfer) preceding the
+ Mailbox DispatchId log in the receipt must not prevent the message id from being decoded."""
+ noise_topic = "0x" + keccak(text="Transfer(address,address,uint256)").hex()
+
+ def logs_with_noise_first(tx):
+ noise = event_log(WBTC, [noise_topic], "0x", log_index=1, tx_hash=tx["hash"])
+ dispatch = dispatch_id_log(MAILBOX, MESSAGE_ID, tx_hash=tx["hash"], log_index=2)
+ return [noise, dispatch] if tx["to"] == Web3.to_checksum_address(ETH_ROUTER) else []
+
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ w3.provider.receipt_logs = logs_with_noise_first
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert result.message_id == Web3.to_hex(MESSAGE_ID)
+
+
+def test_first_dispatch_id_wins_when_receipt_has_two():
+ """veil's ``messageIdFromReceipt`` returns the FIRST matching DispatchId event, not the last."""
+ first_id, second_id = bytes.fromhex("11" * 32), bytes.fromhex("22" * 32)
+
+ def two_dispatch_logs(tx):
+ return [dispatch_id_log(MAILBOX, first_id, tx_hash=tx["hash"], log_index=1),
+ dispatch_id_log(MAILBOX, second_id, tx_hash=tx["hash"], log_index=2)]
+
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ w3.provider.receipt_logs = two_dispatch_logs
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert result.message_id == Web3.to_hex(first_id)
+
+
+def test_dispatch_id_from_a_foreign_address_is_ignored():
+ """``process_receipt`` decodes by topic only — a ``DispatchId`` emitted by anything other than the
+ route's Mailbox is another protocol's event and must read as "no message id"."""
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ w3.provider.receipt_logs = lambda tx: [dispatch_id_log(WBTC, MESSAGE_ID, tx_hash=tx["hash"], log_index=1)]
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert result.message_id is None and result.receipt.id == w3.provider.hash_at(1)
+ assert result.receipt.status == Status.DELIVERY_PENDING and "messageId" not in result.receipt.protocol_state
+
+
+def test_mailbox_dispatch_id_wins_over_an_earlier_foreign_one():
+ """First match *among the Mailbox's own* events, not first match overall."""
+ foreign, mine = bytes.fromhex("11" * 32), bytes.fromhex("22" * 32)
+
+ def logs(tx):
+ return [dispatch_id_log(WBTC, foreign, tx_hash=tx["hash"], log_index=1),
+ dispatch_id_log(MAILBOX, mine, tx_hash=tx["hash"], log_index=2)]
+
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ w3.provider.receipt_logs = logs
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert result.message_id == Web3.to_hex(mine)
+
+
+def test_missing_dispatch_id_log_keeps_tx_hash_as_id():
+ eth, w3 = setup(ETH_ROUTER, with_dispatch_log=False, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ result = eth.transfer_remote("eth", ALEO, amount_atomic=100).send(poll_seconds=0.001)
+ assert result.receipt.status == Status.DELIVERY_PENDING and result.message_id is None
+ assert result.receipt.id == w3.provider.hash_at(1) and "messageId" not in result.receipt.protocol_state
+
+
+def test_build_lists_approval_then_dispatch_without_sending():
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]})
+ txs = eth.transfer_remote("wbtc", ALEO, amount_atomic=100_000).build()
+ assert [t["to"] for t in txs] == [Web3.to_checksum_address(WBTC), Web3.to_checksum_address(WBTC_ROUTER)]
+ assert [t["value"] for t in txs] == [0, 50_000] and w3.provider.sent == []
+
+
+def test_plan_driven_transfer_is_identical_to_the_asset_driven_one():
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]})
+ quote = eth.quote_transfer_remote("wbtc", ALEO, amount_atomic=100_000)
+ by_asset = eth.transfer_remote("wbtc", ALEO, amount_atomic=100_000).build()
+ by_plan = eth.transfer_remote(plan=quote.plan).build()
+ assert by_plan == by_asset and len(by_plan) == 2
+ assert eth.quote_transfer_remote(plan=quote.plan) == quote # the quote round-trips through its own plan
+ assert w3.provider.sent == []
+
+
+def test_plan_driven_transfer_rejects_a_tampered_stale_or_foreign_plan():
+ """Nothing in a caller-supplied plan is trusted, and every rejection happens before any RPC."""
+ eth, w3 = setup(WBTC_ROUTER, quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]})
+ plan = eth.quote_transfer_remote("wbtc", ALEO, amount_atomic=100_000).plan
+ w3.provider.methods.clear()
+ with pytest.raises(BridgeError, match="plan does not match the requested transfer: amount"):
+ eth.transfer_remote(plan=dataclasses.replace(plan, amount_atomic=99_999))
+ with pytest.raises(BridgeError, match="plan does not match the requested transfer: recipient"):
+ eth.transfer_remote(recipient=OTHER_ALEO, plan=plan) # explicit argument vs the plan's own value
+ with pytest.raises(RegistryVersionMismatchError):
+ eth.transfer_remote(plan=dataclasses.replace(plan, registry_version="0000-00-00.stale"))
+ with pytest.raises(RouteUnavailableError, match="not a hyperlane one"):
+ eth.transfer_remote(plan=dataclasses.replace(plan, route_id="xreserve:ethereum/usdc->aleo/usdcx"))
+ with pytest.raises(RouteUnavailableError):
+ eth.transfer_remote(plan=dataclasses.replace(plan, route_id="hyperlane:nowhere/nothing->aleo/eth"))
+ with pytest.raises(ConfigurationError, match="does not match connected account"):
+ eth.transfer_remote(plan=dataclasses.replace(plan, sender=OTHER))
+ with pytest.raises(BridgeError, match="checksummed EVM address"):
+ eth.transfer_remote(plan=dataclasses.replace(plan, sender=None))
+ with pytest.raises(ValueError, match="not both"):
+ eth.transfer_remote("wbtc", plan=plan)
+ with pytest.raises(ValueError, match="not both"):
+ eth.quote_transfer_remote(plan=plan, sender=ACCT.address)
+ assert w3.provider.methods == [] and w3.provider.sent == []
+
+
+def test_read_only_connection_cannot_transfer():
+ w3 = fake_web3(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ eth = make_bridge(ethereum=Ethereum(w3=w3)).eth
+ with pytest.raises(ConfigurationError, match="read-only"):
+ eth.transfer_remote("eth", ALEO, amount_atomic=100)
+
+
+def test_transfer_remote_without_a_plan_or_a_recipient_names_the_missing_recipient():
+ """Same guard as the quote path: a missing recipient is an InvalidRecipientError, not a TypeError."""
+ eth, w3 = setup(ETH_ROUTER, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ eth.transfer_remote("eth", amount_atomic=100)
+ assert w3.provider.methods == [] and w3.provider.sent == []
diff --git a/bridge-sdk/tests/test_eth_hyperlane_quote.py b/bridge-sdk/tests/test_eth_hyperlane_quote.py
new file mode 100644
index 0000000..2bec8b0
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_hyperlane_quote.py
@@ -0,0 +1,155 @@
+import dataclasses
+
+import pytest
+from eth_account import Account
+
+from aleo_bridge.encoding import aleo_address_to_bytes32, bytes32_to_aleo_address
+from aleo_bridge.errors import (AmbiguousRouteError, BridgeError, ChainMismatchError, ConfigurationError,
+ InvalidAmountError, InvalidRecipientError, RouteUnavailableError)
+from aleo_bridge.eth import Ethereum
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import EvmHyperlaneQuote
+from tests.fakes.fake_web3 import ZERO_ADDRESS, fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ALEO_BYTES32 = "b102e0d37e02ec5dbba2460287ac07ef7ea8ee636392ce235402308299901811"
+VEIL_RECIPIENT_BYTES32 = "20e3629764d5338f74bee96675801b1fb29d1fc68b177668f9175708bef84311"
+ETH_ROUTER = "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A"
+WBTC, WBTC_ROUTER = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "0x20CDC85778b732073F7EecEF3DF25c0d310f8772"
+USDT, USDT_ROUTER = "0xdAC17F958D2ee523a2206206994597C13D831ec7", "0x3C2064D78e4578E8F936E3db42aEF044E33FBF31"
+
+
+def eth_module(*, signed=True, **config):
+ w3 = fake_web3(**config)
+ conn = Ethereum(w3=w3, private_key=KEY) if signed else Ethereum(w3=w3)
+ return make_bridge(ethereum=conn).eth, w3
+
+
+def test_native_eth_quote_splits_fee_from_value():
+ eth, _ = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 69_000_000_000_101)]})
+ q = eth.quote_transfer_remote("ethereum/eth", ALEO, amount_atomic=100)
+ assert isinstance(q, EvmHyperlaneQuote) and q.kind == "evm-hyperlane"
+ assert q.plan.route_id == "hyperlane:ethereum/eth->aleo/eth" and q.plan.amount == "0.0000000000000001"
+ assert q.plan.sender == ACCT.address and q.amount_out == "0.0000000000000001"
+ assert q.native_value_atomic == 69_000_000_000_101 and q.native_fee_atomic == 69_000_000_000_001
+ assert q.approval_required is None
+ assert q.recipient_bytes32.hex() == ALEO_BYTES32
+ assert len(q.fees) == 1 and q.fees[0].kind == "network" and q.fees[0].chain_id == "ethereum"
+ assert q.fees[0].asset_id == "ethereum/eth" and q.fees[0].amount == "0.000069000000000001" and q.fees[0].estimated
+
+
+def test_recipient_bytes32_matches_veil_vector():
+ eth, _ = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ recipient = bytes32_to_aleo_address(bytes.fromhex(VEIL_RECIPIENT_BYTES32))
+ q = eth.quote_transfer_remote("eth", recipient, amount_atomic=1)
+ assert q.recipient_bytes32 == bytes.fromhex(VEIL_RECIPIENT_BYTES32)
+ assert aleo_address_to_bytes32(recipient) == q.recipient_bytes32
+ assert q.plan.recipient == recipient
+
+
+def test_collateral_wbtc_quote_reads_allowance():
+ eth, _ = eth_module(quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]},
+ allowances={(WBTC, ACCT.address, WBTC_ROUTER): 0})
+ q = eth.quote_transfer_remote("ethereum/wbtc", ALEO, amount="0.001")
+ assert q.plan.amount_atomic == 100_000 and q.native_value_atomic == 50_000 and q.native_fee_atomic == 50_000
+ assert q.approval_required is True
+ eth, _ = eth_module(quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 100_000)]},
+ allowances={(WBTC, ACCT.address, WBTC_ROUTER): 100_000})
+ assert eth.quote_transfer_remote("wbtc", ALEO, amount_atomic=100_000).approval_required is False
+
+
+def test_read_only_connection_quotes_with_explicit_or_no_sender():
+ eth, _ = eth_module(signed=False, quotes={USDT_ROUTER: [(ZERO_ADDRESS, 50_000), (USDT, 1_000_000)]},
+ allowances={(USDT, ACCT.address, USDT_ROUTER): 1})
+ q = eth.quote_transfer_remote("usdt", ALEO, amount="1")
+ assert q.approval_required is None and q.plan.sender is None
+ q = eth.quote_transfer_remote("usdt", ALEO, amount="1", sender=ACCT.address)
+ assert q.approval_required is True and q.plan.sender == ACCT.address
+
+
+def test_quote_must_cover_amount():
+ eth, _ = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 50)]})
+ with pytest.raises(BridgeError, match="Native Hyperlane quote does not cover"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=100)
+ eth, _ = eth_module(quotes={WBTC_ROUTER: [(ZERO_ADDRESS, 50_000), (WBTC, 99_999)]})
+ with pytest.raises(BridgeError, match="Collateral Hyperlane quote does not cover"):
+ eth.quote_transfer_remote("wbtc", ALEO, amount_atomic=100_000)
+
+
+def test_wrong_chain_is_refused_before_any_contract_read():
+ eth, w3 = eth_module(chain_id=11155111, quotes={ETH_ROUTER: [(ZERO_ADDRESS, 10**15)]})
+ with pytest.raises(ChainMismatchError, match="expected 1"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=1)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_unavailable_unknown_and_explicit_routes():
+ eth, _ = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ with pytest.raises(RouteUnavailableError):
+ eth.quote_transfer_remote("ethereum/usad", ALEO, amount_atomic=1)
+ with pytest.raises(BridgeError):
+ eth.quote_transfer_remote("ethereum/doge", ALEO, amount_atomic=1)
+ with pytest.raises(BridgeError, match="not a Hyperlane route"):
+ eth.quote_transfer_remote("ethereum/usdc", ALEO, amount_atomic=1)
+ route = DEFAULT_REGISTRY.route("hyperlane:ethereum/eth->aleo/eth")
+ assert eth.quote_transfer_remote("eth", ALEO, amount_atomic=1, route=route).plan.route_id == route.id
+
+
+def _corrupted_eth_route(**overrides):
+ route = DEFAULT_REGISTRY.route("hyperlane:ethereum/eth->aleo/eth")
+ return dataclasses.replace(route, metadata={**route.metadata, **overrides})
+
+
+def test_corrupted_router_address_is_refused_before_any_contract_read():
+ eth, w3 = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 10**15)]})
+ route = _corrupted_eth_route(routerAddress="not-an-address")
+ with pytest.raises(ConfigurationError, match="routerAddress"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=1, route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_bad_router_type_is_refused_before_any_contract_read():
+ eth, w3 = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 10**15)]})
+ route = _corrupted_eth_route(routerType="burn")
+ with pytest.raises(ConfigurationError, match="routerType"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=1, route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_out_of_range_destination_domain_is_refused_before_any_contract_read():
+ eth, w3 = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 10**15)]})
+ route = _corrupted_eth_route(destinationDomain=2**32)
+ with pytest.raises(ConfigurationError, match="destinationDomain"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=1, route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_bad_registry_commit_is_refused_before_any_contract_read():
+ eth, w3 = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 10**15)]})
+ route = _corrupted_eth_route(registryCommit="not-hex")
+ with pytest.raises(ConfigurationError, match="registryCommit"):
+ eth.quote_transfer_remote("eth", ALEO, amount_atomic=1, route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_amount_and_recipient_validation():
+ eth, _ = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ with pytest.raises(InvalidAmountError):
+ eth.quote_transfer_remote("eth", ALEO, amount="1", amount_atomic=1)
+ with pytest.raises(InvalidAmountError):
+ eth.quote_transfer_remote("eth", ALEO)
+ with pytest.raises(InvalidRecipientError):
+ eth.quote_transfer_remote("eth", "aleo1notanaddress", amount_atomic=1)
+ with pytest.raises(InvalidRecipientError):
+ eth.quote_transfer_remote("eth", "0x0000000000000000000000000000000000000001", amount_atomic=1)
+
+
+def test_quote_without_a_plan_or_a_recipient_names_the_missing_recipient():
+ """``recipient`` is only optional when ``plan=`` supplies it; otherwise it must be named,
+ not surface as the opaque TypeError ``_recipient_bytes32(None)`` used to raise."""
+ eth, w3 = eth_module(quotes={ETH_ROUTER: [(ZERO_ADDRESS, 1_000)]})
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ eth.quote_transfer_remote("eth", amount_atomic=100)
+ assert w3.provider.methods == [] # refused before any contract read
diff --git a/bridge-sdk/tests/test_eth_recover.py b/bridge-sdk/tests/test_eth_recover.py
new file mode 100644
index 0000000..75ab2b3
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_recover.py
@@ -0,0 +1,318 @@
+import dataclasses
+
+import pytest
+from eth_account import Account
+from web3 import Web3
+
+from aleo_bridge import encoding
+from aleo_bridge.checkpoint import create_checkpoint
+from aleo_bridge.errors import (BridgeError, CheckpointInvalidError, ConfigurationError,
+ RegistryVersionMismatchError)
+from aleo_bridge.eth import LOG_SCAN_CHUNK_BLOCKS, EthModule, Ethereum, _plan_for
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_web3 import deposited_log, dispatch_id_log, fake_web3, make_bridge, sent_transfer_remote_log
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+OTHER = "0x0000000000000000000000000000000000000009"
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ALEO32 = encoding.aleo_address_to_bytes32(ALEO)
+WBTC_ROUTER = "0x20CDC85778b732073F7EecEF3DF25c0d310f8772"
+MAILBOX = "0xc005dc82818d67AF737725bD4bf75435d065D239"
+SEPOLIA_USDC, SEPOLIA_XRESERVE = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", "0x008888878f94C0d87defdf0B07f46B93C1934442"
+REMOTE_TOKEN = bytes.fromhex("b143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401")
+APPROVAL, DISPATCH = "0x" + "11" * 32, "0x" + "22" * 32
+RECOVERED, RECOVERED_2, EARLIER = "0x" + "44" * 32, "0x" + "55" * 32, "0x" + "66" * 32
+RECOVERED_MESSAGE_ID = bytes.fromhex("cd" * 32)
+WBTC_ROUTE = DEFAULT_REGISTRY.route("hyperlane:ethereum/wbtc->aleo/wbtc")
+USDC_ROUTE = DEFAULT_REGISTRY.route("xreserve:sepolia/usdc->aleo-testnet/usdcx")
+WBTC_PLAN = _plan_for(DEFAULT_REGISTRY, WBTC_ROUTE, amount_atomic=100_000, recipient=ALEO, sender=ACCT.address)
+
+
+def hyperlane_checkpoint(plan=WBTC_PLAN, *, tx_id=None):
+ state = {"routeId": WBTC_ROUTE.id, "approvalTxIds": [APPROVAL], "sourceSender": plan.sender,
+ "recipientBytes32": "0x" + ALEO32.hex(), "destinationDomain": 1634493807, "nativeValueAtomic": "50000", "amountAtomic": "100000"}
+ receipt = (Receipt(id=tx_id, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id, protocol_state=state)
+ if tx_id else Receipt(id=APPROVAL, protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING, protocol_state=state))
+ return create_checkpoint(plan, receipt, DEFAULT_REGISTRY)
+
+
+def xreserve_checkpoint(plan, hook: bytes, *, tx_id=None):
+ state = {"routeId": USDC_ROUTE.id, "approvalTxIds": [APPROVAL], "sourceSender": ACCT.address, "mintMode": plan.mint_mode,
+ "intendedRecipient": ALEO, "xReserveContract": SEPOLIA_XRESERVE, "tokenAddress": SEPOLIA_USDC, "sourceChainId": 11155111,
+ "remoteDomain": 10002, "remoteRecipientBytes32": "0x" + ALEO32.hex(), "hookData": "0x" + hook.hex(),
+ "amountAtomic": "2000000", "maxFeeAtomic": "100000"}
+ receipt = (Receipt(id=tx_id, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=tx_id, protocol_state=state)
+ if tx_id else Receipt(id=APPROVAL, protocol="xreserve", status=Status.SOURCE_APPROVAL_PENDING, protocol_state=state))
+ return create_checkpoint(plan, receipt, DEFAULT_REGISTRY)
+
+
+def mainnet_read_only():
+ w3 = fake_web3()
+ return make_bridge(ethereum=Ethereum(w3=w3)).eth, w3
+
+
+def dispatch_history(w3, tx_hash, *, sender=ACCT.address, block_number=0x65, amount=100_000):
+ w3.provider.history_logs.append(sent_transfer_remote_log(WBTC_ROUTER, destination=1634493807, recipient32=ALEO32,
+ amount=amount, tx_hash=tx_hash, block_number=block_number))
+ w3.provider.add_transaction(tx_hash, sender=sender, to=WBTC_ROUTER, block_number=block_number)
+ w3.provider.add_receipt(tx_hash, logs=[dispatch_id_log(MAILBOX, RECOVERED_MESSAGE_ID, tx_hash=tx_hash)],
+ sender=sender, to=WBTC_ROUTER, block_number=block_number)
+
+
+def test_checkpoint_must_match_plan_and_registry():
+ eth, _ = mainnet_read_only()
+ cp = hyperlane_checkpoint()
+ with pytest.raises(CheckpointInvalidError, match="does not match the prepared route"):
+ eth.recover_source(WBTC_PLAN, dataclasses.replace(cp, route={**cp.route, "id": "hyperlane:ethereum/eth->aleo/eth"}))
+ with pytest.raises(RegistryVersionMismatchError):
+ eth.recover_source(WBTC_PLAN, dataclasses.replace(cp, route={**cp.route, "registryVersion": "0000-00-00.stale"}))
+ with pytest.raises(CheckpointInvalidError, match="no submitted transaction"):
+ eth.recover_source(WBTC_PLAN, dataclasses.replace(cp, source=None))
+
+
+def test_hyperlane_rejects_checkpoint_with_destination_leg():
+ eth, _ = mainnet_read_only()
+ cp = hyperlane_checkpoint()
+ bad = dataclasses.replace(cp, destination={"transactionId": "0x" + "77" * 32})
+ with pytest.raises(CheckpointInvalidError, match="destination"):
+ eth.recover_source(WBTC_PLAN, bad)
+
+
+def test_hyperlane_unmined_approval_stays_approval_pending():
+ eth, w3 = mainnet_read_only()
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert receipt.status == Status.SOURCE_APPROVAL_PENDING and receipt.id == APPROVAL and receipt.source_tx_id is None
+ assert receipt.protocol_state["approvalTxIds"] == [APPROVAL] and receipt.protocol_state["sourceSender"] == ACCT.address
+ assert "eth_getLogs" not in w3.provider.methods
+
+
+def test_hyperlane_confirmed_approval_without_dispatch_stops_at_submission_boundary():
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert receipt.status == Status.SOURCE_SUBMISSION_PENDING and receipt.id == APPROVAL
+ assert "eth_getLogs" in w3.provider.methods and w3.provider.sent == []
+
+
+def test_hyperlane_scan_finds_the_dispatch_after_the_approval_block():
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ dispatch_history(w3, EARLIER, block_number=0x10) # before the approval: must be ignored by fromBlock
+ dispatch_history(w3, RECOVERED, block_number=0x66)
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert receipt.status == Status.DELIVERY_PENDING and receipt.source_tx_id == RECOVERED
+ assert receipt.id == Web3.to_hex(RECOVERED_MESSAGE_ID) == receipt.protocol_state["messageId"]
+ assert receipt.protocol_state["approvalTxIds"] == [APPROVAL]
+
+
+def test_hyperlane_scan_ignores_other_senders_and_amounts():
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ dispatch_history(w3, RECOVERED, sender=OTHER)
+ dispatch_history(w3, RECOVERED_2, amount=99_999)
+ assert eth.recover_source(WBTC_PLAN, hyperlane_checkpoint()).status == Status.SOURCE_SUBMISSION_PENDING
+
+
+def test_hyperlane_scan_skips_a_candidate_sent_to_another_contract():
+ """A matching ``SentTransferRemote`` log whose transaction went somewhere other than the router
+ belongs to another call path (a batcher, a router of a different route) — never adopt it."""
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ w3.provider.history_logs.append(sent_transfer_remote_log(WBTC_ROUTER, destination=1634493807, recipient32=ALEO32,
+ amount=100_000, tx_hash=RECOVERED, block_number=0x66))
+ w3.provider.add_transaction(RECOVERED, sender=ACCT.address, to=OTHER, block_number=0x66)
+ w3.provider.add_receipt(RECOVERED, logs=[dispatch_id_log(MAILBOX, RECOVERED_MESSAGE_ID, tx_hash=RECOVERED)],
+ sender=ACCT.address, to=OTHER, block_number=0x66)
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert receipt.status == Status.SOURCE_SUBMISSION_PENDING and receipt.source_tx_id is None
+
+
+def test_recovery_log_scan_is_bounded_and_chunked():
+ """Every eth_getLogs carries an explicit fromBlock/toBlock; the chunks tile the range exactly
+ once and stop at the head read at the start of the scan."""
+ eth, w3 = mainnet_read_only()
+ eth.log_scan_chunk_blocks = 10
+ w3.provider.add_receipt(APPROVAL, block_number=101)
+ w3.provider.block_number = 126 # the approval is 25 blocks behind the head
+ assert eth.recover_source(WBTC_PLAN, hyperlane_checkpoint()).status == Status.SOURCE_SUBMISSION_PENDING
+ assert [(f["fromBlock"], f["toBlock"]) for f in w3.provider.log_filters] == [
+ (hex(101), hex(110)), (hex(111), hex(120)), (hex(121), hex(126))]
+ assert all(f["address"] == [Web3.to_checksum_address(WBTC_ROUTER)] for f in w3.provider.log_filters)
+
+
+def test_a_dispatch_in_the_last_chunk_is_still_found():
+ eth, w3 = mainnet_read_only()
+ eth.log_scan_chunk_blocks = 10
+ w3.provider.add_receipt(APPROVAL, block_number=101)
+ dispatch_history(w3, RECOVERED, block_number=126)
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert receipt.status == Status.DELIVERY_PENDING and receipt.source_tx_id == RECOVERED
+ assert len(w3.provider.log_filters) == 3
+
+
+def test_a_failing_log_chunk_names_the_span_it_could_not_read():
+ eth, w3 = mainnet_read_only()
+ eth.log_scan_chunk_blocks = 10
+ w3.provider.add_receipt(APPROVAL, block_number=101)
+ w3.provider.block_number = 126
+ w3.provider.log_scan_errors[2] = "query returned more than 10000 results"
+ with pytest.raises(BridgeError, match="blocks 111-120 of 101-126") as exc:
+ eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+ assert "10000 results" in str(exc.value) and "smaller" in str(exc.value)
+ assert len(w3.provider.log_filters) == 2 # stopped at the failing chunk
+
+
+def test_xreserve_scan_is_chunked_too():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ eth.log_scan_chunk_blocks = 10
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ cp = xreserve_checkpoint(plan, bytes(65))
+ w3.provider.add_receipt(APPROVAL, block_number=101)
+ w3.provider.block_number = 126
+ assert eth.recover_source(plan, cp).status == Status.SOURCE_SUBMISSION_PENDING
+ assert [(f["fromBlock"], f["toBlock"]) for f in w3.provider.log_filters] == [
+ (hex(101), hex(110)), (hex(111), hex(120)), (hex(121), hex(126))]
+ assert all(f["address"] == [Web3.to_checksum_address(SEPOLIA_XRESERVE)] for f in w3.provider.log_filters)
+
+
+def test_hyperlane_multiple_matches_refuse_to_choose():
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ dispatch_history(w3, RECOVERED)
+ dispatch_history(w3, RECOVERED_2)
+ with pytest.raises(BridgeError, match="Multiple matching Hyperlane dispatches"):
+ eth.recover_source(WBTC_PLAN, hyperlane_checkpoint())
+
+
+def test_hyperlane_required_scan_needs_sender_and_confirmed_approval():
+ eth, w3 = mainnet_read_only()
+ with pytest.raises(BridgeError, match="no confirmed approval block"):
+ eth.recover_source(WBTC_PLAN, hyperlane_checkpoint(), required=True)
+ plan_no_sender = _plan_for(DEFAULT_REGISTRY, WBTC_ROUTE, amount_atomic=100_000, recipient=ALEO, sender=None)
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ with pytest.raises(BridgeError, match="without the source account"):
+ eth.recover_source(plan_no_sender, hyperlane_checkpoint(plan_no_sender), required=True)
+ assert eth.recover_source(plan_no_sender, hyperlane_checkpoint(plan_no_sender)).status == Status.SOURCE_SUBMISSION_PENDING
+
+
+def test_hyperlane_required_scan_with_no_matching_dispatch_is_not_fatal():
+ """A completed scan (known sender, confirmed approval) that matches nothing is a valid answer,
+ not an inability to scan: required=True still returns SOURCE_SUBMISSION_PENDING, never sends."""
+ eth, w3 = mainnet_read_only()
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ receipt = eth.recover_source(WBTC_PLAN, hyperlane_checkpoint(), required=True)
+ assert receipt.status == Status.SOURCE_SUBMISSION_PENDING and receipt.id == APPROVAL
+ assert "eth_getLogs" in w3.provider.methods and w3.provider.sent == []
+
+
+def test_hyperlane_saved_dispatch_is_observed_not_resent():
+ eth, w3 = mainnet_read_only()
+ cp = hyperlane_checkpoint(tx_id=DISPATCH)
+ pending = eth.recover_source(WBTC_PLAN, cp)
+ assert pending.status == Status.SOURCE_CONFIRMING and pending.source_tx_id == DISPATCH
+ w3.provider.add_receipt(DISPATCH, logs=[dispatch_id_log(MAILBOX, RECOVERED_MESSAGE_ID, tx_hash=DISPATCH)], sender=ACCT.address, to=WBTC_ROUTER)
+ done = eth.recover_source(WBTC_PLAN, cp)
+ assert done.status == Status.DELIVERY_PENDING and done.id == Web3.to_hex(RECOVERED_MESSAGE_ID) and w3.provider.sent == []
+
+
+def test_xreserve_scan_recovers_a_confirmed_deposit_from_an_approval_only_checkpoint():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ hook = bytes(65)
+ cp = xreserve_checkpoint(plan, hook)
+ assert cp.source["hookData"] == "0x" + hook.hex()
+ assert eth.recover_source(plan, cp).status == Status.SOURCE_APPROVAL_PENDING
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ assert eth.recover_source(plan, cp).status == Status.SOURCE_SUBMISSION_PENDING
+ log = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=ACCT.address, remote_recipient32=ALEO32, value=2_000_000,
+ remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000, hook_data=hook, tx_hash=RECOVERED, log_index=3)
+ w3.provider.history_logs.append(log)
+ w3.provider.add_receipt(RECOVERED, logs=[log], sender=ACCT.address, to=SEPOLIA_XRESERVE)
+ other = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=OTHER, remote_recipient32=ALEO32, value=2_000_000,
+ remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000, hook_data=hook, tx_hash=RECOVERED_2, log_index=1)
+ w3.provider.history_logs.append(other)
+ w3.provider.add_receipt(RECOVERED_2, logs=[other], sender=OTHER, to=SEPOLIA_XRESERVE)
+ receipt = eth.recover_source(plan, cp)
+ assert receipt.status == Status.ATTESTATION_PENDING and receipt.source_tx_id == RECOVERED
+ nonce = encoding.xreserve_deposit_nonce(0, bytes.fromhex(RECOVERED[2:]), 3)
+ assert receipt.protocol_state["nonce"] == "0x" + nonce.hex() and receipt.id == receipt.protocol_state["messageHash"]
+
+
+def test_xreserve_multiple_matches_refuse_to_choose():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ hook = bytes(65)
+ cp = xreserve_checkpoint(plan, hook)
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ log = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=ACCT.address, remote_recipient32=ALEO32, value=2_000_000,
+ remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000, hook_data=hook, tx_hash=RECOVERED, log_index=3)
+ w3.provider.history_logs.append(log)
+ w3.provider.add_receipt(RECOVERED, logs=[log], sender=ACCT.address, to=SEPOLIA_XRESERVE)
+ log2 = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=ACCT.address, remote_recipient32=ALEO32, value=2_000_000,
+ remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000, hook_data=hook, tx_hash=RECOVERED_2, log_index=1)
+ w3.provider.history_logs.append(log2)
+ w3.provider.add_receipt(RECOVERED_2, logs=[log2], sender=ACCT.address, to=SEPOLIA_XRESERVE)
+ with pytest.raises(BridgeError, match="Multiple matching xReserve deposits"):
+ eth.recover_source(plan, cp)
+
+
+def test_xreserve_required_scan_needs_confirmed_approval():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ cp = xreserve_checkpoint(plan, bytes(65))
+ with pytest.raises(BridgeError, match="no confirmed approval block"):
+ eth.recover_source(plan, cp, required=True)
+
+
+def test_xreserve_required_scan_with_no_matching_deposit_is_not_fatal():
+ """Same ruling for xReserve: a completed scan (known sender, confirmed approval) that matches
+ nothing is a valid answer, not an inability to scan."""
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ cp = xreserve_checkpoint(plan, bytes(65))
+ w3.provider.add_receipt(APPROVAL, block_number=0x65)
+ receipt = eth.recover_source(plan, cp, required=True)
+ assert receipt.status == Status.SOURCE_SUBMISSION_PENDING and receipt.id == APPROVAL
+ assert "eth_getLogs" in w3.provider.methods and w3.provider.sent == []
+
+
+def test_xreserve_private_recovery_uses_checkpointed_hook_and_wrapper_recipient():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ plan = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address, mint_mode="private")
+ hook = encoding.xreserve_hook_data("private", ALEO, "testnet", "7scalar")
+ wrapper32 = encoding.aleo_address_to_bytes32(encoding.aleo_program_address("shielded_usdcx_wrapper.aleo", "testnet"))
+ cp = xreserve_checkpoint(plan, hook)
+ cp = dataclasses.replace(cp, source={**cp.source, "transactionId": DISPATCH})
+ log = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=ACCT.address, remote_recipient32=wrapper32, value=2_000_000,
+ remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000, hook_data=hook, tx_hash=DISPATCH, log_index=2)
+ w3.provider.add_receipt(DISPATCH, logs=[log], sender=ACCT.address, to=SEPOLIA_XRESERVE)
+ receipt = eth.recover_source(plan, cp)
+ assert receipt.status == Status.ATTESTATION_PENDING and receipt.protocol_state["hookData"] == "0x" + hook.hex()
+ assert receipt.protocol_state["remoteRecipientBytes32"] == "0x" + wrapper32.hex() and receipt.protocol_state["mintMode"] == "private"
+ bad = dataclasses.replace(cp, source={**cp.source, "hookData": "0x02"})
+ with pytest.raises(CheckpointInvalidError, match="hook data"):
+ eth.recover_source(plan, bad)
+
+
+def test_log_scan_chunk_blocks_cannot_be_lowered_below_one():
+ """``_scan_logs`` advances by ``chunk`` blocks a pass, so a 0 (or negative) chunk would spin
+ forever on a live range: the attribute validates on assignment, exactly like the constructor
+ (``ConfigurationError``, the error the constructor has always raised for this — the plan's
+ ``ValueError`` wording is kept as the same single check rather than two different errors)."""
+ eth, _ = mainnet_read_only()
+ for bad in (0, -5):
+ with pytest.raises(ConfigurationError, match="at least 1"):
+ eth.log_scan_chunk_blocks = bad
+ assert eth.log_scan_chunk_blocks == LOG_SCAN_CHUNK_BLOCKS # the refused assignments changed nothing
+ eth.log_scan_chunk_blocks = 10
+ assert eth.log_scan_chunk_blocks == 10
+ with pytest.raises(ConfigurationError, match="at least 1"):
+ EthModule(make_bridge(ethereum=Ethereum(w3=fake_web3())), Ethereum(w3=fake_web3()), log_scan_chunk_blocks=0)
diff --git a/bridge-sdk/tests/test_eth_status.py b/bridge-sdk/tests/test_eth_status.py
new file mode 100644
index 0000000..98c9d44
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_status.py
@@ -0,0 +1,167 @@
+import pytest
+from eth_account import Account
+from web3 import Web3
+
+from aleo_bridge import encoding
+from aleo_bridge.errors import (BridgeError, ChainMismatchError, CheckpointInvalidError, ConfigurationError,
+ UnsupportedRouteError)
+from aleo_bridge.eth import Ethereum, _plan_for
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_web3 import deposited_log, dispatch_id_log, fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ALEO32 = encoding.aleo_address_to_bytes32(ALEO)
+WBTC, WBTC_ROUTER = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "0x20CDC85778b732073F7EecEF3DF25c0d310f8772"
+MAILBOX = "0xc005dc82818d67AF737725bD4bf75435d065D239"
+SEPOLIA_USDC, SEPOLIA_XRESERVE = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", "0x008888878f94C0d87defdf0B07f46B93C1934442"
+REMOTE_TOKEN = bytes.fromhex("b143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401")
+H1, H2 = "0x" + "11" * 32, "0x" + "22" * 32
+MESSAGE_ID = bytes.fromhex("ab" * 32)
+DELIVERED_ID = "0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d"
+WBTC_ROUTE = DEFAULT_REGISTRY.route("hyperlane:ethereum/wbtc->aleo/wbtc")
+USDC_ROUTE = DEFAULT_REGISTRY.route("xreserve:sepolia/usdc->aleo-testnet/usdcx")
+
+
+def hyperlane_state(**overrides):
+ state = {"routeId": WBTC_ROUTE.id, "approvalTxIds": [H1], "sourceSender": ACCT.address,
+ "recipientBytes32": "0x" + ALEO32.hex(), "destinationDomain": 1634493807,
+ "nativeValueAtomic": "50000", "amountAtomic": "100000"}
+ state.update(overrides)
+ return state
+
+
+def xreserve_state(**overrides):
+ state = {"routeId": USDC_ROUTE.id, "approvalTxIds": [H1], "sourceSender": ACCT.address, "mintMode": "public",
+ "intendedRecipient": ALEO, "xReserveContract": SEPOLIA_XRESERVE, "tokenAddress": SEPOLIA_USDC,
+ "sourceChainId": 11155111, "remoteDomain": 10002, "remoteRecipientBytes32": "0x" + ALEO32.hex(),
+ "hookData": "0x" + "00" * 65, "amountAtomic": "2000000", "maxFeeAtomic": "100000"}
+ state.update(overrides)
+ return state
+
+
+def mainnet(*, signed=True, **config):
+ w3 = fake_web3(**config)
+ conn = Ethereum(w3=w3, private_key=KEY) if signed else Ethereum(w3=w3)
+ return make_bridge(ethereum=conn).eth, w3
+
+
+WBTC_PLAN = _plan_for(DEFAULT_REGISTRY, WBTC_ROUTE, amount_atomic=100_000, recipient=ALEO, sender=ACCT.address)
+USDC_PLAN = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+
+
+def test_approval_pending_branch():
+ eth, w3 = mainnet()
+ receipt = Receipt(id=H1, protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING, protocol_state=hyperlane_state())
+ assert eth.source_status(WBTC_PLAN, receipt) is receipt # no receipt yet → unchanged
+ w3.provider.add_receipt(H1)
+ advanced = eth.source_status(WBTC_PLAN, receipt)
+ assert advanced.status == Status.SOURCE_SUBMISSION_PENDING and advanced.id == H1
+ w3.provider.add_receipt(H1, status=0)
+ failed = eth.source_status(WBTC_PLAN, receipt)
+ assert failed.status == Status.FAILED and failed.protocol_state["sourceError"] == f"EVM approval transaction reverted: {H1}"
+ assert "eth_sendRawTransaction" not in w3.provider.methods
+
+
+def test_hyperlane_source_confirming_branch():
+ eth, w3 = mainnet()
+ receipt = Receipt(id=H2, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=H2, protocol_state=hyperlane_state())
+ assert eth.source_status(WBTC_PLAN, receipt) is receipt
+ w3.provider.add_receipt(H2, logs=[dispatch_id_log(MAILBOX, MESSAGE_ID, tx_hash=H2)], sender=ACCT.address, to=WBTC_ROUTER)
+ advanced = eth.source_status(WBTC_PLAN, receipt)
+ assert advanced.status == Status.DELIVERY_PENDING and advanced.id == Web3.to_hex(MESSAGE_ID)
+ assert advanced.source_tx_id == H2 and advanced.protocol_state["messageId"] == Web3.to_hex(MESSAGE_ID)
+ w3.provider.add_receipt(H2) # confirmed, no DispatchId log
+ no_id = eth.source_status(WBTC_PLAN, receipt)
+ assert no_id.status == Status.DELIVERY_PENDING and no_id.id == H2 and "messageId" not in no_id.protocol_state
+ w3.provider.add_receipt(H2, status=0)
+ failed = eth.source_status(WBTC_PLAN, receipt)
+ assert failed.status == Status.FAILED and failed.protocol_state["sourceError"] == f"EVM transaction reverted: {H2}"
+
+
+def test_hyperlane_state_must_match_plan():
+ eth, _ = mainnet()
+ bad = Receipt(id=H2, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=H2,
+ protocol_state=hyperlane_state(amountAtomic="1"))
+ with pytest.raises(CheckpointInvalidError, match="does not match the prepared transfer"):
+ eth.source_status(WBTC_PLAN, bad)
+ wrong_route = Receipt(id=H2, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=H2,
+ protocol_state=hyperlane_state(routeId="hyperlane:ethereum/eth->aleo/eth"))
+ with pytest.raises(CheckpointInvalidError, match="does not match the prepared route"):
+ eth.source_status(WBTC_PLAN, wrong_route)
+ missing_hash = Receipt(id=H2, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, protocol_state=hyperlane_state())
+ with pytest.raises(CheckpointInvalidError, match="source transaction id"):
+ eth.source_status(WBTC_PLAN, missing_hash)
+ other = Receipt(id=H2, protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id=H2, protocol_state=hyperlane_state())
+ with pytest.raises(BridgeError, match="SOURCE_APPROVAL_PENDING and SOURCE_CONFIRMING"):
+ eth.source_status(WBTC_PLAN, other)
+
+
+def test_xreserve_source_confirming_branch_works_read_only():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth # no signer: uses sourceSender
+ receipt = Receipt(id=H2, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=H2, protocol_state=xreserve_state())
+ assert eth.source_status(USDC_PLAN, receipt) is receipt
+ log = deposited_log(SEPOLIA_XRESERVE, local_token=SEPOLIA_USDC, depositor=ACCT.address, remote_recipient32=ALEO32,
+ value=2_000_000, remote_domain=10002, remote_token32=REMOTE_TOKEN, max_fee=100_000,
+ hook_data=bytes(65), tx_hash=H2, log_index=3)
+ w3.provider.add_receipt(H2, logs=[log], sender=ACCT.address, to=SEPOLIA_XRESERVE)
+ advanced = eth.source_status(USDC_PLAN, receipt)
+ nonce = encoding.xreserve_deposit_nonce(0, bytes.fromhex(H2[2:]), 3)
+ assert advanced.status == Status.ATTESTATION_PENDING and advanced.protocol_state["nonce"] == "0x" + nonce.hex()
+ assert advanced.id == advanced.protocol_state["messageHash"] and advanced.protocol_state["depositLogIndex"] == 3
+ assert advanced.protocol_state["approvalTxIds"] == [H1] and advanced.source_tx_id == H2
+ w3.provider.add_receipt(H2, status=0)
+ assert eth.source_status(USDC_PLAN, receipt).status == Status.FAILED
+
+
+def test_xreserve_state_validation():
+ w3 = fake_web3(chain_id=11155111)
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3, private_key=KEY)).eth
+ for bad in (xreserve_state(mintMode="private"), xreserve_state(intendedRecipient="aleo1" + "q" * 58),
+ xreserve_state(hookData="0x00"), xreserve_state(amountAtomic="abc")):
+ receipt = Receipt(id=H2, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=H2, protocol_state=bad)
+ with pytest.raises(CheckpointInvalidError):
+ eth.source_status(USDC_PLAN, receipt)
+ no_owner = Receipt(id=H2, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=H2,
+ protocol_state=xreserve_state(sourceSender=None))
+ plan_without_sender = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=None)
+ read_only = make_bridge(environment="testnet", ethereum=Ethereum(w3=fake_web3(chain_id=11155111))).eth
+ with pytest.raises(ConfigurationError, match="prepared sender"):
+ read_only.source_status(plan_without_sender, no_owner)
+
+
+def test_xreserve_source_confirming_asserts_the_chain_before_reading():
+ """Mirrors the Hyperlane branch: a connection pointed at the wrong network must not read
+ receipts or logs from it."""
+ w3 = fake_web3() # chain 1, route wants 11155111
+ eth = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3)).eth
+ receipt = Receipt(id=H2, protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id=H2,
+ protocol_state=xreserve_state())
+ with pytest.raises(ChainMismatchError):
+ eth.source_status(USDC_PLAN, receipt)
+ assert "eth_getTransactionReceipt" not in w3.provider.methods and "eth_getLogs" not in w3.provider.methods
+
+
+def test_is_delivered_reads_mailbox():
+ eth, w3 = mainnet(delivered={DELIVERED_ID})
+ assert eth.is_delivered(DELIVERED_ID) is True
+ assert eth.is_delivered(bytes.fromhex(DELIVERED_ID[2:])) is True
+ assert eth.is_delivered("0x" + "00" * 32) is False
+ assert w3.provider.methods.count("eth_call") == 3
+ with pytest.raises(BridgeError, match="32-byte message id"):
+ eth.is_delivered("0x1234")
+ testnet = make_bridge(environment="testnet", ethereum=Ethereum(w3=fake_web3(chain_id=11155111))).eth
+ with pytest.raises(UnsupportedRouteError, match="Mailbox"):
+ testnet.is_delivered(DELIVERED_ID)
+
+
+def test_balance_native_and_erc20():
+ eth, _ = mainnet(eth_balances={ACCT.address: 5}, token_balances={(WBTC, ACCT.address): 7})
+ assert eth.balance("eth") == 5 and eth.balance("ethereum/wbtc") == 7 and eth.balance("usdt") == 0
+ read_only, _ = mainnet(signed=False, eth_balances={ACCT.address: 5})
+ with pytest.raises(ConfigurationError, match="address"):
+ read_only.balance("eth")
+ assert read_only.balance("eth", address=ACCT.address) == 5
diff --git a/bridge-sdk/tests/test_eth_xreserve_execute.py b/bridge-sdk/tests/test_eth_xreserve_execute.py
new file mode 100644
index 0000000..450c2ae
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_xreserve_execute.py
@@ -0,0 +1,226 @@
+import dataclasses
+import json
+
+import pytest
+from eth_abi import decode
+from eth_account import Account
+from eth_utils import keccak
+from web3 import Web3
+
+from aleo_bridge import encoding
+from aleo_bridge.errors import (BridgeError, ConfigurationError, InvalidRecipientError,
+ RegistryVersionMismatchError, RouteUnavailableError)
+from aleo_bridge.eth import Ethereum
+from aleo_bridge.types import DepositReceipt, Status
+from tests.fakes.fake_web3 import deposited_log, fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+USDC = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
+XRESERVE = "0x008888878f94C0d87defdf0B07f46B93C1934442"
+OTHER = "0x0000000000000000000000000000000000000009"
+REMOTE_TOKEN = bytes.fromhex("b143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401")
+APPROVE = keccak(text="approve(address,uint256)")[:4].hex()
+DEPOSIT = keccak(text="depositToRemote(uint256,uint32,bytes32,address,uint256,bytes)")[:4].hex()
+
+
+def deposit_fields(tx):
+ """The DepositedToRemote fields the contract would emit for this depositToRemote calldata."""
+ value, remote_domain, remote_recipient, local_token, max_fee, hook = decode(
+ ["uint256", "uint32", "bytes32", "address", "uint256", "bytes"], bytes.fromhex(tx["data"][10:]))
+ return {"local_token": Web3.to_checksum_address(local_token), "depositor": tx["from"],
+ "remote_recipient32": remote_recipient, "value": value, "remote_domain": remote_domain,
+ "remote_token32": REMOTE_TOKEN, "max_fee": max_fee, "hook_data": hook}
+
+
+def deposit_logs(*, log_index=3, **overrides):
+ """Echo the depositToRemote calldata back as a DepositedToRemote log, optionally corrupting fields."""
+ def logs(tx):
+ if tx["to"] != Web3.to_checksum_address(XRESERVE) or tx["data"][2:10] != DEPOSIT:
+ return []
+ return [deposited_log(XRESERVE, tx_hash=tx["hash"], log_index=log_index, **{**deposit_fields(tx), **overrides})]
+ return logs
+
+
+def setup(*, allowance=0, logs=None, checkpoints=None):
+ w3 = fake_web3(chain_id=11155111, token_balances={(USDC, ACCT.address): 3_000_000},
+ allowances={(USDC, ACCT.address, XRESERVE): allowance})
+ w3.provider.receipt_logs = logs or deposit_logs()
+ bridge = make_bridge(environment="testnet", ethereum=Ethereum(w3=w3, private_key=KEY), checkpoints=checkpoints)
+ return bridge.eth, w3
+
+
+def test_build_lists_approve_then_deposit_with_zero_value():
+ eth, w3 = setup()
+ txs = eth.deposit_usdc(ALEO, amount="2", mint_mode="record").build()
+ assert [t["to"] for t in txs] == [Web3.to_checksum_address(USDC), Web3.to_checksum_address(XRESERVE)]
+ assert txs[0]["data"][2:10] == APPROVE and txs[1]["data"][2:10] == DEPOSIT
+ assert txs[0]["value"] == 0 and txs[1]["value"] == 0 and w3.provider.sent == []
+
+
+def test_record_mode_deposit_derives_nonce_payload_and_message_hash():
+ eth, w3 = setup()
+ seen = []
+ result = eth.deposit_usdc(ALEO, amount="2", mint_mode="record").send(poll_seconds=0.001, on_checkpoint=seen.append)
+ assert isinstance(result, DepositReceipt)
+ sent = w3.provider.sent
+ assert len(sent) == 2 and sent[1]["value"] == 0 and sent[1]["to"] == Web3.to_checksum_address(XRESERVE)
+ assert sent[0]["data"][2:].lower() == APPROVE + XRESERVE[2:].lower().rjust(64, "0") + format(2_000_000, "064x")
+ receipt = result.receipt
+ assert receipt.status == Status.ATTESTATION_PENDING and receipt.protocol == "xreserve"
+ assert receipt.source_tx_id == w3.provider.hash_at(2) == result.transaction_id
+ hook = b"\x01" + bytes(64)
+ recipient32 = encoding.aleo_address_to_bytes32(ALEO)
+ nonce = encoding.xreserve_deposit_nonce(0, bytes.fromhex(w3.provider.hash_at(2)[2:]), 3)
+ payload = encoding.xreserve_deposit_payload(amount=2_000_000, remote_domain=10002, remote_token=REMOTE_TOKEN,
+ remote_recipient=recipient32, local_token=USDC, depositor=ACCT.address,
+ max_fee=100_000, nonce=nonce, hook_data=hook)
+ message_hash = "0x" + encoding.xreserve_message_hash(payload).hex()
+ assert len(payload) == 305
+ assert receipt.id == message_hash == result.message_hash == receipt.protocol_state["messageHash"]
+ assert result.nonce == "0x" + nonce.hex() == receipt.protocol_state["nonce"]
+ assert receipt.protocol_state["payload"] == "0x" + payload.hex()
+ state = receipt.protocol_state
+ assert state["routeId"] == "xreserve:sepolia/usdc->aleo-testnet/usdcx" and state["approvalTxIds"] == [w3.provider.hash_at(1)]
+ assert state["sourceSender"] == ACCT.address and state["mintMode"] == "record" and state["intendedRecipient"] == ALEO
+ assert state["xReserveContract"] == Web3.to_checksum_address(XRESERVE) and state["tokenAddress"] == Web3.to_checksum_address(USDC)
+ assert state["sourceChainId"] == 11155111 and state["sourceDomain"] == 0 and state["remoteDomain"] == 10002
+ assert state["remoteRecipientBytes32"] == "0x" + recipient32.hex() and state["hookData"] == "0x" + hook.hex()
+ assert state["amountAtomic"] == "2000000" and state["maxFeeAtomic"] == "100000" and state["depositLogIndex"] == 3
+ assert state["bridgeProgram"] == "test_usdcx_bridge_v2.aleo" and state["wrapperProgram"] == "shielded_usdcx_wrapper.aleo"
+ assert [cp.source for cp in seen] == [
+ {"approvalTransactionIds": [w3.provider.hash_at(1)], "hookData": "0x" + hook.hex()},
+ {"approvalTransactionIds": [w3.provider.hash_at(1)], "transactionId": w3.provider.hash_at(2), "hookData": "0x" + hook.hex()},
+ {"approvalTransactionIds": [w3.provider.hash_at(1)], "transactionId": w3.provider.hash_at(2), "hookData": "0x" + hook.hex()},
+ ]
+ assert seen[-1].id == message_hash and seen[-1].intent["mintMode"] == "record"
+
+
+def test_sufficient_allowance_skips_approval():
+ eth, w3 = setup(allowance=5_000_000)
+ result = eth.deposit_usdc(ALEO, amount_atomic=2_000_000).send(poll_seconds=0.001)
+ assert len(w3.provider.sent) == 1 and result.receipt.protocol_state["approvalTxIds"] == []
+ assert result.receipt.protocol_state["hookData"] == "0x" + "00" * 65
+
+
+def test_private_mode_deposits_to_wrapper_and_never_persists_the_secret():
+ eth, _ = setup(allowance=5_000_000)
+ seen = []
+ result = eth.deposit_usdc(ALEO, amount="2", mint_mode="private", secret_nonce="7scalar").send(
+ poll_seconds=0.001, on_checkpoint=seen.append)
+ wrapper32 = encoding.aleo_address_to_bytes32(encoding.aleo_program_address("shielded_usdcx_wrapper.aleo", "testnet"))
+ state = result.receipt.protocol_state
+ assert state["remoteRecipientBytes32"] == "0x" + wrapper32.hex() and state["intendedRecipient"] == ALEO
+ assert state["hookData"] == "0x" + encoding.xreserve_hook_data("private", ALEO, "testnet", "7scalar").hex()
+ assert "7scalar" not in json.dumps(state) and all("7scalar" not in cp.to_json() for cp in seen)
+ assert seen[0].source["hookData"].startswith("0x02") and len(seen[0].source["hookData"]) == 132
+
+
+def test_event_mismatch_or_absence_raises():
+ eth, _ = setup(allowance=5_000_000, logs=deposit_logs(value=1))
+ with pytest.raises(BridgeError, match="does not match the prepared transfer"):
+ eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+ eth, _ = setup(allowance=5_000_000, logs=deposit_logs(remote_token32=bytes(32)))
+ with pytest.raises(BridgeError, match="does not match the prepared transfer"):
+ eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+ eth, _ = setup(allowance=5_000_000, logs=lambda tx: [])
+ with pytest.raises(BridgeError, match="DepositedToRemote"):
+ eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+
+
+@pytest.mark.parametrize("field, corrupted", [
+ ("local_token", OTHER),
+ ("depositor", OTHER),
+ ("value", 1_999_999),
+ ("remote_domain", 10_003),
+ ("remote_recipient32", bytes(32)),
+ ("remote_token32", bytes(32)),
+ ("max_fee", 99_999),
+ ("hook_data", b"\x01" + bytes(64)),
+])
+def test_every_re_verified_deposit_field_must_match(field, corrupted):
+ """All eight canonical DepositedToRemote fields are load-bearing: corrupting any one of them
+ alone must make the event stop counting as this transfer's deposit."""
+ eth, _ = setup(allowance=5_000_000, logs=deposit_logs(**{field: corrupted}))
+ with pytest.raises(BridgeError, match="does not match the prepared transfer"):
+ eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+
+
+@pytest.mark.parametrize("ours_first", [True, False])
+def test_our_deposit_event_is_selected_among_other_accounts_deposits(ours_first):
+ """A batched transaction carries several accounts' deposits; ours is whichever event matches all
+ eight fields, not whichever happens to be last in the receipt."""
+ def logs(tx):
+ if tx["to"] != Web3.to_checksum_address(XRESERVE) or tx["data"][2:10] != DEPOSIT:
+ return []
+ fields = deposit_fields(tx)
+ ours = deposited_log(XRESERVE, tx_hash=tx["hash"], log_index=3, **fields)
+ theirs = deposited_log(XRESERVE, tx_hash=tx["hash"], log_index=7, **{**fields, "depositor": OTHER})
+ return [ours, theirs] if ours_first else [theirs, ours]
+
+ eth, _ = setup(allowance=5_000_000, logs=logs)
+ result = eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+ assert result.receipt.status == Status.ATTESTATION_PENDING
+ assert result.receipt.protocol_state["depositLogIndex"] == 3 # ours, whatever the order
+
+
+def test_timeouts_return_pending_receipts():
+ eth, w3 = setup()
+ w3.provider.pending_nth.add(1)
+ result = eth.deposit_usdc(ALEO, amount="2").send(timeout_seconds=0.01, poll_seconds=0.001)
+ assert result.receipt.status == Status.SOURCE_APPROVAL_PENDING and result.receipt.source_tx_id is None
+ assert result.receipt.id == w3.provider.hash_at(1) and result.message_hash == "" and result.nonce == ""
+ assert len(w3.provider.sent) == 1
+ eth, w3 = setup(allowance=5_000_000)
+ w3.provider.pending_nth.add(1)
+ seen = []
+ result = eth.deposit_usdc(ALEO, amount="2").send(timeout_seconds=0.01, poll_seconds=0.001, on_checkpoint=seen.append)
+ assert result.receipt.status == Status.SOURCE_CONFIRMING and result.receipt.source_tx_id == w3.provider.hash_at(1)
+ assert [cp.source for cp in seen] == [{"transactionId": w3.provider.hash_at(1), "hookData": "0x" + "00" * 65}]
+
+
+def test_plan_driven_deposit_is_identical_to_the_recipient_driven_one():
+ eth, w3 = setup(allowance=5_000_000)
+ quote = eth.quote_deposit_usdc(ALEO, amount="2", mint_mode="record")
+ by_recipient = eth.deposit_usdc(ALEO, amount="2", mint_mode="record").build()
+ by_plan = eth.deposit_usdc(plan=quote.plan).build() # mint_mode comes from the plan
+ assert by_plan == by_recipient and len(by_plan) == 1 and by_plan[0]["data"][2:10] == DEPOSIT
+ assert eth.quote_deposit_usdc(plan=quote.plan) == quote
+ assert w3.provider.sent == []
+
+
+def test_plan_driven_deposit_rejects_a_tampered_stale_or_foreign_plan():
+ eth, w3 = setup(allowance=5_000_000)
+ plan = eth.quote_deposit_usdc(ALEO, amount="2").plan
+ w3.provider.methods.clear()
+ with pytest.raises(BridgeError, match="plan does not match the requested transfer: amount"):
+ eth.deposit_usdc(plan=dataclasses.replace(plan, amount_atomic=2_000_001))
+ with pytest.raises(BridgeError, match="plan does not match the requested transfer: steps"):
+ eth.deposit_usdc(plan=dataclasses.replace(plan, mint_mode="private")) # steps still say "protocol"
+ with pytest.raises(BridgeError, match="plan does not match the requested transfer: mint_mode"):
+ eth.deposit_usdc(mint_mode="record", plan=plan)
+ with pytest.raises(RegistryVersionMismatchError):
+ eth.deposit_usdc(plan=dataclasses.replace(plan, registry_version="0000-00-00.stale"))
+ with pytest.raises(RouteUnavailableError, match="not a xreserve one"):
+ eth.deposit_usdc(plan=dataclasses.replace(plan, route_id="hyperlane:ethereum/eth->aleo/eth"))
+ with pytest.raises(ConfigurationError, match="does not match connected account"):
+ eth.deposit_usdc(plan=dataclasses.replace(plan, sender=OTHER))
+ with pytest.raises(ValueError, match="not both"):
+ eth.quote_deposit_usdc(ALEO, amount="2", sender=ACCT.address, plan=plan)
+ assert w3.provider.methods == [] and w3.provider.sent == []
+
+
+def test_reverted_deposit_raises():
+ eth, w3 = setup(allowance=5_000_000)
+ w3.provider.reverted_nth.add(1)
+ with pytest.raises(BridgeError, match="reverted"):
+ eth.deposit_usdc(ALEO, amount="2").send(poll_seconds=0.001)
+
+
+def test_deposit_without_a_plan_or_a_recipient_names_the_missing_recipient():
+ """Same guard as the quote path: a missing recipient is an InvalidRecipientError, not a TypeError."""
+ eth, w3 = setup()
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ eth.deposit_usdc(amount="2")
+ assert w3.provider.methods == [] and w3.provider.sent == []
diff --git a/bridge-sdk/tests/test_eth_xreserve_quote.py b/bridge-sdk/tests/test_eth_xreserve_quote.py
new file mode 100644
index 0000000..532defd
--- /dev/null
+++ b/bridge-sdk/tests/test_eth_xreserve_quote.py
@@ -0,0 +1,152 @@
+import dataclasses
+
+import pytest
+from eth_account import Account
+
+from aleo_bridge.encoding import aleo_address_to_bytes32, aleo_program_address, xreserve_hook_data
+from aleo_bridge.errors import (BridgeError, ChainMismatchError, ConfigurationError, InsufficientBalanceError,
+ InvalidAmountError, InvalidRecipientError)
+from aleo_bridge.eth import Ethereum
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import EvmXReserveQuote
+from tests.fakes.fake_web3 import fake_web3, make_bridge
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+SEPOLIA_USDC = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
+SEPOLIA_XRESERVE = "0x008888878f94C0d87defdf0B07f46B93C1934442"
+MAINNET_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
+MAINNET_XRESERVE = "0x8888888199b2Df864bf678259607d6D5EBb4e3Ce"
+
+
+def sepolia(*, signed=True, balance=3_000_000, allowance=0, chain_id=11155111):
+ w3 = fake_web3(chain_id=chain_id, token_balances={(SEPOLIA_USDC, ACCT.address): balance},
+ allowances={(SEPOLIA_USDC, ACCT.address, SEPOLIA_XRESERVE): allowance})
+ conn = Ethereum(w3=w3, private_key=KEY) if signed else Ethereum(w3=w3)
+ return make_bridge(environment="testnet", ethereum=conn).eth, w3
+
+
+def test_record_mode_quote():
+ eth, _ = sepolia()
+ q = eth.quote_deposit_usdc(ALEO, amount="2", mint_mode="record")
+ assert isinstance(q, EvmXReserveQuote) and q.kind == "evm-xreserve"
+ assert q.plan.route_id == "xreserve:sepolia/usdc->aleo-testnet/usdcx" and q.plan.mint_mode == "record"
+ assert q.plan.amount == "2" and q.plan.amount_atomic == 2_000_000 and q.plan.sender == ACCT.address
+ assert q.amount_out == "2" and q.fees == ()
+ assert q.hook_data == b"\x01" + bytes(64)
+ assert q.remote_recipient_bytes32 == aleo_address_to_bytes32(ALEO)
+ assert q.balance_atomic == 3_000_000 and q.allowance_atomic == 0 and q.approval_required is True
+ assert q.max_fee_atomic == 100_000
+
+
+def test_public_mode_is_default_and_allowance_can_cover():
+ eth, _ = sepolia(allowance=5_000_000)
+ q = eth.quote_deposit_usdc(ALEO, amount_atomic=2_000_000)
+ assert q.plan.mint_mode == "public" and q.hook_data == bytes(65) and q.approval_required is False
+
+
+def test_private_mode_targets_wrapper_program_and_commits_recipient():
+ eth, _ = sepolia()
+ q = eth.quote_deposit_usdc(ALEO, amount="2", mint_mode="private", secret_nonce="7scalar")
+ wrapper = aleo_program_address("shielded_usdcx_wrapper.aleo", "testnet")
+ assert q.remote_recipient_bytes32 == aleo_address_to_bytes32(wrapper)
+ assert q.hook_data[0] == 2 and len(q.hook_data) == 65 and q.hook_data[1:33] != bytes(32)
+ assert q.hook_data == xreserve_hook_data("private", ALEO, "testnet", "7scalar")
+ assert q.hook_data != eth.quote_deposit_usdc(ALEO, amount="2", mint_mode="private").hook_data
+ assert q.plan.recipient == ALEO # the plan keeps the intended recipient, not the wrapper
+
+
+def test_minimum_amount_and_balance_are_enforced():
+ eth, _ = sepolia()
+ with pytest.raises(InvalidAmountError, match="minimum deposit is 2000000"):
+ eth.quote_deposit_usdc(ALEO, amount_atomic=1_999_999)
+ with pytest.raises(InsufficientBalanceError, match="USDC"):
+ eth.quote_deposit_usdc(ALEO, amount_atomic=3_000_001)
+
+
+def test_wrong_chain_and_bad_mint_mode():
+ eth, w3 = sepolia(chain_id=1)
+ with pytest.raises(ChainMismatchError, match="expected 11155111"):
+ eth.quote_deposit_usdc(ALEO, amount="2")
+ assert "eth_call" not in w3.provider.methods
+ eth, _ = sepolia()
+ with pytest.raises(BridgeError, match="mint_mode"):
+ eth.quote_deposit_usdc(ALEO, amount="2", mint_mode="shielded")
+
+
+def test_read_only_needs_explicit_sender():
+ eth, _ = sepolia(signed=False)
+ with pytest.raises(ConfigurationError, match="sender"):
+ eth.quote_deposit_usdc(ALEO, amount="2")
+ q = eth.quote_deposit_usdc(ALEO, amount="2", sender=ACCT.address)
+ assert q.balance_atomic == 3_000_000 and q.plan.sender == ACCT.address
+
+
+def test_mainnet_environment_selects_ethereum_route():
+ w3 = fake_web3(chain_id=1, token_balances={(MAINNET_USDC, ACCT.address): 2_000_000},
+ allowances={(MAINNET_USDC, ACCT.address, MAINNET_XRESERVE): 0})
+ eth = make_bridge(environment="mainnet", ethereum=Ethereum(w3=w3, private_key=KEY)).eth
+ q = eth.quote_deposit_usdc(ALEO, amount="2")
+ assert q.plan.route_id == "xreserve:ethereum/usdc->aleo/usdcx" and q.approval_required is True
+
+
+def _corrupted_xreserve_route(**overrides):
+ route = DEFAULT_REGISTRY.route("xreserve:sepolia/usdc->aleo-testnet/usdcx")
+ return dataclasses.replace(route, metadata={**route.metadata, **overrides})
+
+
+def test_corrupted_xreserve_contract_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(xReserveContract="not-an-address")
+ with pytest.raises(ConfigurationError, match="xReserveContract"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_non_hex_remote_token_bytes32_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(remoteTokenBytes32="not-hex")
+ with pytest.raises(ConfigurationError, match="remoteTokenBytes32"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_short_remote_token_bytes32_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(remoteTokenBytes32="0x" + "ab" * 16) # 16 bytes, not 32
+ with pytest.raises(ConfigurationError, match="remoteTokenBytes32"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_bridge_program_without_aleo_suffix_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(bridgeProgram="not_a_program")
+ with pytest.raises(ConfigurationError, match="bridgeProgram"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_negative_remote_domain_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(remoteDomain=-1)
+ with pytest.raises(ConfigurationError, match="remoteDomain"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_non_digit_minimum_amount_atomic_is_refused_before_any_contract_read():
+ eth, w3 = sepolia()
+ route = _corrupted_xreserve_route(minimumAmountAtomic="2_000_000")
+ with pytest.raises(ConfigurationError, match="minimumAmountAtomic"):
+ eth.quote_deposit_usdc(ALEO, amount="2", route=route)
+ assert "eth_call" not in w3.provider.methods
+
+
+def test_quote_without_a_plan_or_a_recipient_names_the_missing_recipient():
+ """``recipient`` is only optional when ``plan=`` supplies it (the hook commits to it)."""
+ eth, w3 = sepolia()
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ eth.quote_deposit_usdc(amount="2")
+ assert w3.provider.methods == [] # refused before any contract read
diff --git a/bridge-sdk/tests/test_evm_call.py b/bridge-sdk/tests/test_evm_call.py
new file mode 100644
index 0000000..9dbb670
--- /dev/null
+++ b/bridge-sdk/tests/test_evm_call.py
@@ -0,0 +1,383 @@
+import pytest
+from eth_account import Account
+from web3 import Web3
+
+from aleo_bridge._calls import EvmCall, EvmOutcome, EvmStep
+from aleo_bridge._plan import build_plan
+from aleo_bridge._evm_abi import ERC20_ABI, WARP_ROUTE_ABI
+from aleo_bridge.checkpoint import FileCheckpointStore
+from aleo_bridge.errors import BridgeError, ConfigurationError
+from aleo_bridge.eth import Ethereum, _plan_for
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import DispatchReceipt, Receipt, Status
+from tests.fakes.fake_web3 import fake_web3
+
+KEY = "0x" + "11" * 32
+ACCT = Account.from_key(KEY)
+ALEO = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
+ROUTER = "0x20CDC85778b732073F7EecEF3DF25c0d310f8772"
+ROUTE = DEFAULT_REGISTRY.route("hyperlane:ethereum/wbtc->aleo/wbtc")
+USDC_ROUTE = DEFAULT_REGISTRY.route("xreserve:ethereum/usdc->aleo/usdcx")
+
+
+def make_call(w3, *, sender=None, store=None, approvals=1, steps=None):
+ conn = Ethereum(w3=w3, private_key=KEY)
+ plan = _plan_for(DEFAULT_REGISTRY, ROUTE, amount_atomic=100_000, recipient=ALEO, sender=sender)
+ token = w3.eth.contract(address=Web3.to_checksum_address(WBTC), abi=ERC20_ABI)
+ warp = w3.eth.contract(address=Web3.to_checksum_address(ROUTER), abi=WARP_ROUTE_ABI)
+
+ def default_steps(owner):
+ out = [EvmStep("approve", token.address, token.encode_abi("approve", args=[warp.address, 100_000]), 0)
+ for _ in range(approvals)]
+ out.append(EvmStep("main", warp.address, warp.encode_abi("transferRemote", args=[1634493807, b"\x11" * 32, 100_000]), 50_000))
+ return out
+
+ def finish(outcome: EvmOutcome) -> DispatchReceipt:
+ status = Status.DELIVERY_PENDING if outcome.status == "CONFIRMED" else Status(outcome.status)
+ rid = outcome.source_tx_id or (outcome.approval_tx_ids[-1] if outcome.approval_tx_ids else "unsent")
+ receipt = Receipt(id=rid, protocol="hyperlane", status=status, source_tx_id=outcome.source_tx_id,
+ protocol_state={"routeId": ROUTE.id, "approvalTxIds": list(outcome.approval_tx_ids),
+ "sourceSender": outcome.sender})
+ return DispatchReceipt(transaction_id=rid, route_id=ROUTE.id, message_id=None, amount_atomic=100_000, receipt=receipt)
+
+ return EvmCall(conn, plan=plan, registry=DEFAULT_REGISTRY, steps=steps or default_steps,
+ finish=finish, store=store), plan
+
+
+def test_plan_for_hyperlane_and_xreserve_shapes():
+ plan = _plan_for(DEFAULT_REGISTRY, ROUTE, amount_atomic=100_000, recipient=ALEO, sender=ACCT.address)
+ assert plan.route_id == ROUTE.id and plan.registry_version == DEFAULT_REGISTRY.version
+ assert plan.protocol == "hyperlane" and plan.environment == "mainnet"
+ assert plan.source_asset_id == "ethereum/wbtc" and plan.destination_asset_id == "aleo/wbtc"
+ assert plan.amount == "0.001" and plan.amount_atomic == 100_000 and plan.recipient == ALEO
+ assert plan.sender == ACCT.address and plan.mint_mode == "public"
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in plan.steps] == [
+ ("source-approval", "approve", "evm-wallet", False),
+ ("source-dispatch", "dispatch", "evm-wallet", True),
+ ("message-delivery", "wait-delivery", "protocol", False),
+ ("destination-confirmation", "confirm-delivery", "protocol", False),
+ ]
+ eth_plan = _plan_for(DEFAULT_REGISTRY, DEFAULT_REGISTRY.route("hyperlane:ethereum/eth->aleo/eth"),
+ amount_atomic=100, recipient=ALEO, sender=None)
+ assert [s.id for s in eth_plan.steps][0] == "source-dispatch" # native: no approval step
+ assert eth_plan.amount == "0.0000000000000001"
+ private = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=None, mint_mode="private")
+ assert private.amount == "2" and private.mint_mode == "private"
+ assert [(s.id, s.executor, s.irreversible) for s in private.steps] == [
+ ("source-approval", "evm-wallet", False), ("source-deposit", "evm-wallet", True),
+ ("deposit-attestation", "protocol", False), ("destination-mint", "aleo-wallet", False),
+ ]
+ public = _plan_for(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=None)
+ assert public.steps[-1].executor == "protocol"
+ with pytest.raises(BridgeError, match="mint_mode"):
+ _plan_for(DEFAULT_REGISTRY, ROUTE, amount_atomic=1, recipient=ALEO, sender=None, mint_mode="private")
+
+
+def test_build_plan_is_the_shared_builder_eth_re_exports():
+ from aleo_bridge import _plan, eth
+
+ assert eth._plan_for is _plan.build_plan
+
+
+def test_build_plan_derives_the_wallet_executor_from_the_source_chain_family():
+ """Ethereum-origin plans are identical to the ones the hard-coded "evm-wallet" used to produce."""
+ eth_plan = build_plan(DEFAULT_REGISTRY, DEFAULT_REGISTRY.route("hyperlane:ethereum/eth->aleo/eth"),
+ amount_atomic=100, recipient=ALEO, sender=ACCT.address)
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in eth_plan.steps] == [
+ ("source-dispatch", "dispatch", "evm-wallet", True),
+ ("message-delivery", "wait-delivery", "protocol", False),
+ ("destination-confirmation", "confirm-delivery", "protocol", False),
+ ]
+ usdc_plan = build_plan(DEFAULT_REGISTRY, USDC_ROUTE, amount_atomic=2_000_000, recipient=ALEO, sender=ACCT.address)
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in usdc_plan.steps] == [
+ ("source-approval", "approve", "evm-wallet", False),
+ ("source-deposit", "deposit", "evm-wallet", True),
+ ("deposit-attestation", "wait-attestation", "protocol", False),
+ ("destination-mint", "mint", "protocol", False),
+ ]
+
+
+def test_build_plan_uses_a_solana_wallet_for_a_solana_origin():
+ plan = build_plan(DEFAULT_REGISTRY, DEFAULT_REGISTRY.route("hyperlane:solana/sol->aleo/sol"),
+ amount_atomic=1, recipient=ALEO, sender="11111111111111111111111111111111")
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in plan.steps] == [
+ ("source-dispatch", "dispatch", "solana-wallet", True), # SOL is native: no approval step
+ ("message-delivery", "wait-delivery", "protocol", False),
+ ("destination-confirmation", "confirm-delivery", "protocol", False),
+ ]
+
+
+def test_build_plan_generalizes_xreserve_to_an_aleo_origin_burn():
+ """Aleo-origin xReserve withdrawal is a different step shape than the EVM-origin deposit."""
+ plan = build_plan(DEFAULT_REGISTRY, DEFAULT_REGISTRY.route("xreserve:aleo/usdcx->ethereum/usdc"),
+ amount_atomic=10_000_000, recipient=WBTC, sender=None)
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in plan.steps] == [
+ ("source-burn", "burn", "aleo-wallet", True),
+ ("withdrawal-attestation", "wait-attestation", "protocol", False),
+ ("destination-withdrawal", "withdraw", "protocol", False),
+ ("destination-confirmation", "confirm-delivery", "protocol", False),
+ ]
+
+
+def test_build_plan_skips_approval_for_aleo_token_sources_over_hyperlane():
+ """Aleo ARC-20 tokens (wBTC/USDT mirrored on Aleo) need no approval, unlike their EVM counterparts."""
+ plan = build_plan(DEFAULT_REGISTRY, DEFAULT_REGISTRY.route("hyperlane:aleo/wbtc->ethereum/wbtc"),
+ amount_atomic=1, recipient=WBTC, sender=None)
+ assert [(s.id, s.kind, s.executor, s.irreversible) for s in plan.steps] == [
+ ("source-dispatch", "dispatch", "aleo-wallet", True),
+ ("message-delivery", "wait-delivery", "protocol", False),
+ ("destination-confirmation", "confirm-delivery", "protocol", False),
+ ]
+
+
+def test_build_returns_unsigned_dicts_in_order_without_sending():
+ w3 = fake_web3()
+ call, _ = make_call(w3)
+ txs = call.build()
+ assert [t["to"] for t in txs] == [Web3.to_checksum_address(WBTC), Web3.to_checksum_address(ROUTER)]
+ assert [t["value"] for t in txs] == [0, 50_000]
+ assert [t["nonce"] for t in txs] == [0, 1]
+ assert all(t["from"] == ACCT.address and t["chainId"] == 1 for t in txs)
+ assert txs[1]["data"].startswith("0x") and w3.provider.sent == []
+
+
+def test_send_runs_approve_then_main_and_checkpoints_each_hash_before_polling():
+ w3 = fake_web3()
+ # Make both receipts resolve only after a couple of pending polls, so the ordering
+ # test actually exercises "checkpoint fires, THEN polling happens" rather than a
+ # same-tick resolution that would pass even if the code checkpointed after polling.
+ w3.provider.receipt_delay_nth[1] = 2
+ w3.provider.receipt_delay_nth[2] = 2
+ call, plan = make_call(w3)
+ seen = []
+ poll_counts_at_checkpoint = []
+
+ def on_checkpoint(cp):
+ seen.append(cp)
+ poll_counts_at_checkpoint.append(dict(w3.provider.receipt_poll_counts))
+
+ result = call.send(on_checkpoint=on_checkpoint, poll_seconds=0.001)
+ assert isinstance(result, DispatchReceipt) and result.receipt.status == Status.DELIVERY_PENDING
+ assert [t["to"] for t in w3.provider.sent] == [Web3.to_checksum_address(WBTC), Web3.to_checksum_address(ROUTER)]
+ assert w3.provider.sent[1]["value"] == 50_000
+ assert result.receipt.protocol_state["approvalTxIds"] == [w3.provider.hash_at(1)] and result.receipt.source_tx_id == w3.provider.hash_at(2)
+ assert [cp.source for cp in seen] == [
+ {"approvalTransactionIds": [w3.provider.hash_at(1)]},
+ {"approvalTransactionIds": [w3.provider.hash_at(1)], "transactionId": w3.provider.hash_at(2)},
+ {"approvalTransactionIds": [w3.provider.hash_at(1)], "transactionId": w3.provider.hash_at(2)},
+ ]
+ assert all(cp.route == {"id": plan.route_id, "registryVersion": plan.registry_version} for cp in seen)
+ assert seen[0].intent["sender"] == ACCT.address
+
+ # Checkpoint-before-poll ordering, per hash:
+ # cp0 (approval broadcast) fires before any eth_getTransactionReceipt for hash1.
+ assert poll_counts_at_checkpoint[0].get(w3.provider.hash_at(1), 0) == 0
+ assert w3.provider.hash_at(2) not in poll_counts_at_checkpoint[0]
+ # cp1 (main broadcast) fires after hash1 was fully polled to confirmation, but
+ # before any eth_getTransactionReceipt for hash2.
+ assert poll_counts_at_checkpoint[1].get(w3.provider.hash_at(1), 0) > 0
+ assert poll_counts_at_checkpoint[1].get(w3.provider.hash_at(2), 0) == 0
+ # cp2 (confirmed) fires only after hash2 has itself been polled.
+ assert poll_counts_at_checkpoint[2].get(w3.provider.hash_at(2), 0) > 0
+ # ... and each hash's poll count strictly increases after its own checkpoint fired
+ # (the receipt_delay=2 knob forces at least one more poll beyond the checkpoint tick).
+ assert w3.provider.receipt_poll_counts[w3.provider.hash_at(1)] > poll_counts_at_checkpoint[0].get(w3.provider.hash_at(1), 0)
+ assert w3.provider.receipt_poll_counts[w3.provider.hash_at(2)] > poll_counts_at_checkpoint[1].get(w3.provider.hash_at(2), 0)
+
+ # Full RPC sequence: approve is sent and fully confirmed (>=1 receipt poll) before
+ # the main call is ever broadcast, and the main call is polled only afterwards.
+ relevant = [m for m in w3.provider.methods if m in ("eth_sendRawTransaction", "eth_getTransactionReceipt")]
+ first_send = relevant.index("eth_sendRawTransaction")
+ second_send = relevant.index("eth_sendRawTransaction", first_send + 1)
+ assert relevant[first_send] == "eth_sendRawTransaction"
+ between = relevant[first_send + 1:second_send]
+ assert between and all(m == "eth_getTransactionReceipt" for m in between)
+ after = relevant[second_send + 1:]
+ assert after and all(m == "eth_getTransactionReceipt" for m in after)
+
+
+def test_approval_timeout_returns_pending_and_stops():
+ w3 = fake_web3()
+ w3.provider.pending_nth.add(1)
+ call, _ = make_call(w3)
+ result = call.send(timeout_seconds=0.01, poll_seconds=0.001)
+ assert result.receipt.status == Status.SOURCE_APPROVAL_PENDING and result.receipt.source_tx_id is None
+ assert result.receipt.protocol_state["approvalTxIds"] == [w3.provider.hash_at(1)] and len(w3.provider.sent) == 1
+
+
+def test_main_timeout_returns_source_confirming():
+ w3 = fake_web3()
+ w3.provider.pending_nth.add(1)
+ call, _ = make_call(w3, approvals=0)
+ result = call.send(timeout_seconds=0.01, poll_seconds=0.001)
+ assert result.receipt.status == Status.SOURCE_CONFIRMING and result.receipt.source_tx_id == w3.provider.hash_at(1)
+
+
+def test_wait_false_returns_after_first_broadcast():
+ w3 = fake_web3()
+ call, _ = make_call(w3)
+ result = call.send(wait=False)
+ assert result.receipt.status == Status.SOURCE_APPROVAL_PENDING and len(w3.provider.sent) == 1
+
+
+def test_reverted_transaction_raises():
+ w3 = fake_web3()
+ w3.provider.reverted_nth.add(1)
+ call, _ = make_call(w3)
+ with pytest.raises(BridgeError) as exc:
+ call.send(poll_seconds=0.001)
+ assert str(exc.value) == f"EVM transaction reverted: {w3.provider.hash_at(1)}"
+
+
+def test_plan_sender_must_match_connected_account():
+ w3 = fake_web3()
+ call, _ = make_call(w3, sender="0x0000000000000000000000000000000000000001")
+ with pytest.raises(ConfigurationError, match="does not match connected account"):
+ call.send()
+ assert w3.provider.sent == []
+
+
+def test_build_rejects_a_plan_sender_that_is_not_the_connected_account():
+ w3 = fake_web3()
+ call, _ = make_call(w3, sender="0x0000000000000000000000000000000000000001")
+ with pytest.raises(ConfigurationError, match="does not match connected account"):
+ call.build()
+ assert w3.provider.sent == []
+
+
+class ExplodingStore:
+ """A checkpoint store whose disk is full / read-only."""
+
+ def __init__(self):
+ self.attempts = []
+
+ def save(self, checkpoint):
+ self.attempts.append(checkpoint)
+ raise OSError("read-only file system")
+
+ def load(self, checkpoint_id): # pragma: no cover - never reached
+ return None
+
+ def list(self): # pragma: no cover - never reached
+ return []
+
+ def delete(self, checkpoint_id): # pragma: no cover - never reached
+ return None
+
+
+def test_store_failure_after_broadcast_reports_the_tx_hash_and_never_hides_it():
+ """The transaction is already on the wire: the caller's callback must have run first, the
+ error must name the hash and the checkpoint, and no receipt poll may follow the failure."""
+ w3 = fake_web3()
+ store = ExplodingStore()
+ call, _ = make_call(w3, store=store, approvals=0)
+ seen = []
+ with pytest.raises(BridgeError) as exc:
+ call.send(on_checkpoint=seen.append, poll_seconds=0.001)
+ message = str(exc.value)
+ assert w3.provider.hash_at(1) in message and "broadcast" in message and "checkpoint" in message.lower()
+ assert [cp.id for cp in seen] == [w3.provider.hash_at(1)] # callback ran before the store
+ assert [cp.id for cp in store.attempts] == [w3.provider.hash_at(1)]
+ assert len(w3.provider.sent) == 1 # broadcast happened exactly once
+ assert "eth_getTransactionReceipt" not in w3.provider.methods # nothing polled after the failure
+
+
+def test_a_call_is_single_use_once_it_has_broadcast():
+ """Re-sending the same call would spend the approval and dispatch a second transfer."""
+ w3 = fake_web3()
+ call, _ = make_call(w3, approvals=0)
+ call.send(poll_seconds=0.001)
+ tx_hash = w3.provider.hash_at(1)
+ with pytest.raises(BridgeError) as exc:
+ call.send(poll_seconds=0.001)
+ assert str(exc.value) == (f"this call already broadcast {tx_hash}; use bridge.eth.source_status(plan, receipt) "
+ "to follow it — do not resend")
+ assert len(w3.provider.sent) == 1
+
+
+def test_the_guard_is_armed_by_the_first_broadcast_of_a_multi_step_send():
+ """The approval alone is enough: a resend would re-approve and re-dispatch."""
+ w3 = fake_web3()
+ w3.provider.pending_nth.add(1)
+ call, _ = make_call(w3) # approval + main
+ call.send(timeout_seconds=0.01, poll_seconds=0.001) # stops pending after the approval
+ with pytest.raises(BridgeError, match=f"already broadcast {w3.provider.hash_at(1)}"):
+ call.send(poll_seconds=0.001)
+ assert len(w3.provider.sent) == 1
+
+
+def test_a_lost_send_response_also_arms_the_single_use_guard():
+ w3 = fake_web3()
+ w3.provider.send_errors[1] = "connection reset by peer"
+ call, _ = make_call(w3, approvals=0)
+ with pytest.raises(BridgeError, match="may have been broadcast"):
+ call.send(poll_seconds=0.001)
+ with pytest.raises(BridgeError, match="already broadcast"):
+ call.send(poll_seconds=0.001)
+ assert w3.provider.methods.count("eth_sendRawTransaction") == 1
+
+
+def test_a_failure_before_any_broadcast_leaves_the_call_usable():
+ w3 = fake_web3()
+ failures = []
+
+ def steps(owner):
+ if not failures:
+ failures.append(owner)
+ raise BridgeError("router quote unavailable") # a read failed; nothing was sent
+ return [EvmStep("main", Web3.to_checksum_address(ROUTER), "0x", 0)]
+
+ call, _ = make_call(w3, steps=steps)
+ with pytest.raises(BridgeError, match="router quote unavailable"):
+ call.send()
+ assert w3.provider.sent == []
+ result = call.send(poll_seconds=0.001) # the guard was never armed
+ assert result.receipt.status == Status.DELIVERY_PENDING and len(w3.provider.sent) == 1
+
+
+def test_build_stays_repeatable_after_a_send():
+ w3 = fake_web3()
+ call, _ = make_call(w3, approvals=0)
+ call.send(poll_seconds=0.001)
+ assert [t["to"] for t in call.build()] == [Web3.to_checksum_address(ROUTER)] # a preview never spends
+ assert len(w3.provider.sent) == 1
+
+
+def test_a_mismatched_echoed_hash_stops_the_call_before_any_checkpoint():
+ """Checkpointing the node's hash would strand the real transaction under an id nobody can find."""
+ w3 = fake_web3()
+ w3.provider.echo_hashes[1] = "0x" + "ab" * 32
+ store = ExplodingStore()
+ call, _ = make_call(w3, store=store, approvals=0)
+ seen = []
+ with pytest.raises(BridgeError) as exc:
+ call.send(on_checkpoint=seen.append, poll_seconds=0.001)
+ assert "0x" + "ab" * 32 in str(exc.value) and w3.provider.hash_at(1) in str(exc.value)
+ assert seen == [] and store.attempts == []
+ assert "eth_getTransactionReceipt" not in w3.provider.methods
+
+
+def test_a_mismatched_echoed_hash_also_arms_the_single_use_guard():
+ """The node answered, so the signed bytes may be in its mempool under the hash we computed —
+ the same ambiguity as a lost response, and the same reason not to resend."""
+ w3 = fake_web3()
+ w3.provider.echo_hashes[1] = "0x" + "ab" * 32
+ call, _ = make_call(w3, approvals=0)
+ with pytest.raises(BridgeError, match="echoed transaction hash"):
+ call.send(poll_seconds=0.001)
+ local_hash = w3.provider.hash_at(1)
+ with pytest.raises(BridgeError) as exc:
+ call.send(poll_seconds=0.001)
+ assert str(exc.value) == (f"this call already broadcast {local_hash}; use bridge.eth.source_status(plan, receipt) "
+ "to follow it — do not resend")
+ assert w3.provider.methods.count("eth_sendRawTransaction") == 1
+
+
+def test_bound_store_saves_every_checkpoint(tmp_path):
+ w3 = fake_web3()
+ store = FileCheckpointStore(tmp_path)
+ call, _ = make_call(w3, store=store)
+ result = call.send(poll_seconds=0.001)
+ ids = {cp.id for cp in store.list()}
+ assert w3.provider.hash_at(1) in ids and result.receipt.id in ids
diff --git a/bridge-sdk/tests/test_execute.py b/bridge-sdk/tests/test_execute.py
new file mode 100644
index 0000000..ced7214
--- /dev/null
+++ b/bridge-sdk/tests/test_execute.py
@@ -0,0 +1,325 @@
+"""``lifecycle.execute`` — the verb that commits funds on the source chain.
+
+The invariants under test: every source leg is dispatched through the module's own ``plan=``
+surface (never a re-derived asset/recipient/amount), an Aleo leg proves → checkpoints the exact
+transaction → broadcasts → checkpoints the id (so a crash between proving and broadcast is
+recoverable), and every boundary reaches the caller's callback once and the bound store once.
+"""
+import dataclasses
+import json
+
+import pytest
+
+from aleo_bridge.checkpoint import FileCheckpointStore
+from aleo_bridge.errors import ConfigurationError, RouteUnavailableError
+from aleo_bridge.lifecycle import execute, prepare
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+
+PREPARED_TX = {"preparedTransaction": {"transactionId": "at1fake1",
+ "serializedTransaction": json.dumps(
+ {"type": "execute", "id": "at1fake1", "fee": {}})}}
+
+
+def _aleo_eth_plan(b, recipient=EVM_ADDRESS):
+ return prepare(b.registry, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=recipient)
+
+
+def _usdc_plan(b, **kw):
+ return prepare(b.registry, source="ethereum/usdc", destination="aleo/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, **kw)
+
+
+def _wbtc_plan(b, **kw):
+ return prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001",
+ recipient=ALEO_RECIPIENT, **kw)
+
+
+def _sol_plan(b, **kw):
+ return prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, **kw)
+
+
+# ── Aleo-origin legs ──────────────────────────────────────────────────────────
+
+def test_aleo_hyperlane_checkpoints_prepared_tx_before_broadcast_and_requotes_igp():
+ b = FakeBridge(ethereum=False)
+ plan = _aleo_eth_plan(b)
+ cps = []
+ progress = execute(b, plan, on_checkpoint=cps.append)
+
+ assert b.calls[0] == ("hyperlane.quote_gas_payment", "aleo/eth") # re-quoted at the last moment
+ assert b.calls[1][1]["gas_payment_microcredits"] == 8_174_147
+ assert b.calls[1][1]["as_signer"] is False and b.calls[1][1]["amount_atomic"] == 1
+ kinds = [e[0] for e in b.events]
+ assert kinds == ["delegate_prepared", "checkpoint:SOURCE_SUBMISSION_PENDING", "submit",
+ "checkpoint:SOURCE_CONFIRMING"]
+ assert b.events[2][2] is False # submit_prepared(wait=False)
+ assert cps[0].source == PREPARED_TX
+ assert cps[1].source == {"transactionId": "at1fake1"}
+ assert "deliveryVerification" not in cps[0].to_dict() # no ETH connection → no baseline
+ assert progress.next == "wait" and progress.receipt.status is Status.SOURCE_CONFIRMING
+ assert progress.receipt.source_tx_id == "at1fake1"
+ assert progress.receipt.protocol_state["routeId"] == plan.route_id
+
+
+def test_aleo_hyperlane_pinned_gas_and_signer_mode_and_local_proving():
+ b = FakeBridge(ethereum=False)
+ execute(b, _aleo_eth_plan(b), gas_payment_microcredits=1, mode="signer", proving="local")
+ assert b.calls[0][0] == "hyperlane.transfer_remote" # no quote call
+ assert b.calls[0][1]["gas_payment_microcredits"] == 1 and b.calls[0][1]["as_signer"] is True
+ assert [e[0] for e in b.events][0] == "prove"
+ with pytest.raises(ConfigurationError, match="proving"):
+ execute(b, _aleo_eth_plan(b), gas_payment_microcredits=1, proving="wallet")
+ with pytest.raises(ConfigurationError, match="mode"):
+ execute(b, _aleo_eth_plan(b), gas_payment_microcredits=1, mode="private")
+
+
+def test_aleo_hyperlane_captures_destination_balance_baseline_for_own_recipient():
+ b = FakeBridge() # ethereum configured, address == recipient
+ b.eth.balances["ethereum/eth"] = 100
+ plan = _aleo_eth_plan(b)
+ cps = []
+ progress = execute(b, plan, gas_payment_microcredits=1, on_checkpoint=cps.append)
+ # read before the dispatch is even built, so the pre-broadcast checkpoint can carry it
+ assert b.calls[0] == ("eth.balance", "ethereum/eth")
+ assert b.calls[1][0] == "hyperlane.transfer_remote"
+ assert cps[0].delivery_verification == {"balanceBeforeAtomic": "100", "expectedIncreaseAtomic": "1"}
+ assert progress.receipt.protocol_state["destinationBalanceBeforeAtomic"] == "100"
+ assert progress.receipt.protocol_state["expectedDestinationIncreaseAtomic"] == "1"
+ # a recipient that is not our connection's address gets no baseline (we cannot read its balance)
+ b2 = FakeBridge()
+ cps2 = []
+ execute(b2, _aleo_eth_plan(b2, recipient="0x0000000000000000000000000000000000000002"),
+ gas_payment_microcredits=1, on_checkpoint=cps2.append)
+ assert cps2[0].delivery_verification is None and ("eth.balance", "ethereum/eth") not in b2.calls
+
+
+def test_aleo_xreserve_burn_modes_and_private_inputs():
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/usdcx", destination="ethereum/usdc", amount="2.5",
+ recipient=EVM_ADDRESS)
+ cps = []
+ progress = execute(b, plan, record="{ owner: aleo1..., amount: 3000000u128.private }",
+ merkle_proof="[{ siblings: [...], leaf_index: 1u32 }, { ... }]", on_checkpoint=cps.append)
+ assert b.calls[0][0] == "xreserve.burn"
+ assert b.calls[0][1]["mode"] == "private" and b.calls[0][1]["amount_atomic"] == 2_500_000
+ assert b.calls[0][1]["record"].startswith("{ owner") and b.calls[0][1]["merkle_proof"].startswith("[")
+ assert [c.source for c in cps] == [PREPARED_TX, {"transactionId": "at1fake1"}]
+ assert progress.receipt.protocol_state["burnMode"] == "private"
+ b2 = FakeBridge(ethereum=False)
+ execute(b2, plan, mode="public-as-signer")
+ assert b2.calls[0][1]["mode"] == "public-as-signer"
+ with pytest.raises(ConfigurationError, match="mode"):
+ execute(b2, plan, mode="signer")
+
+
+# ── EVM- and Solana-origin legs ───────────────────────────────────────────────
+
+def test_evm_hyperlane_forwards_intermediate_checkpoints_and_polling_controls():
+ b = FakeBridge()
+ plan = _wbtc_plan(b, sender=EVM_ADDRESS)
+ approval = Receipt(id="0x" + "11" * 32, protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": ["0x" + "11" * 32],
+ "sourceSender": EVM_ADDRESS})
+ b.eth.intermediates = [approval]
+ cps = []
+ progress = execute(b, plan, on_checkpoint=cps.append, poll_seconds=2.0, timeout_seconds=300.0)
+ assert b.calls == [("eth.transfer_remote", {"plan": plan})] # the module re-derives nothing
+ assert ("evm_send", 300.0, 2.0) in b.events
+ assert [c.source for c in cps] == [{"approvalTransactionIds": ["0x" + "11" * 32]},
+ {"transactionId": "0x" + "aa" * 32}]
+ assert progress.next == "wait" and progress.receipt.status is Status.SOURCE_CONFIRMING
+
+
+def test_evm_xreserve_passes_mint_mode_and_secret_nonce():
+ b = FakeBridge()
+ plan = _usdc_plan(b, mint_mode="private")
+ progress = execute(b, plan, secret_nonce="7scalar")
+ assert b.calls[0] == ("eth.deposit_usdc", {"plan": plan, "secret_nonce": "7scalar"})
+ assert progress.receipt.status is Status.ATTESTATION_PENDING and progress.next == "wait"
+ assert progress.receipt.protocol_state["mintMode"] == "private"
+ assert "secretNonce" not in json.dumps(progress.receipt.protocol_state)
+ assert "7scalar" not in json.dumps(progress.receipt.protocol_state)
+
+
+def test_a_private_mint_without_a_secret_nonce_is_refused_before_any_rpc():
+ """``secret_nonce`` is not a plan field and the SDK never stores it: a private deposit that
+ silently fell back to "0scalar" would mint to a commitment the caller cannot ever reproduce."""
+ b = FakeBridge()
+ with pytest.raises(ConfigurationError, match="secret_nonce"):
+ execute(b, _usdc_plan(b, mint_mode="private"))
+ assert b.calls == []
+ execute(b, _usdc_plan(b)) # public mint: the default is fine
+ assert b.calls[0][1]["secret_nonce"] == "0scalar"
+ b2 = FakeBridge()
+ execute(b2, _usdc_plan(b2, mint_mode="record"))
+ assert b2.calls[0][1]["secret_nonce"] == "0scalar"
+
+
+def test_solana_hyperlane():
+ b = FakeBridge(solana=True)
+ plan = _sol_plan(b, sender=SOL_ADDRESS)
+ cps = []
+ progress = execute(b, plan, on_checkpoint=cps.append)
+ assert b.calls == [("sol.transfer_remote", {"plan": plan})]
+ assert cps[-1].source["blockhash"] == "recent" and cps[-1].source["lastValidBlockHeight"] == "123456789"
+ assert progress.receipt.status is Status.SOURCE_CONFIRMING
+
+
+# ── refusals ──────────────────────────────────────────────────────────────────
+
+def test_sender_mismatch_missing_connection_and_unavailable_route():
+ b = FakeBridge()
+ with pytest.raises(ConfigurationError, match="sender"):
+ execute(b, _wbtc_plan(b, sender="0x0000000000000000000000000000000000000009"))
+ with pytest.raises(ConfigurationError, match="Solana connection"):
+ execute(b, _sol_plan(b))
+ with pytest.raises(RouteUnavailableError):
+ execute(b, prepare(b.registry, source="aleo/aleo", destination="ethereum/aleo", amount="1",
+ recipient=EVM_ADDRESS))
+ no_eth = FakeBridge(ethereum=False) # bridge.ethereum is None → bridge.eth must not be touched
+ with pytest.raises(ConfigurationError, match="Ethereum connection"):
+ execute(no_eth, _wbtc_plan(no_eth))
+ assert b.calls == [] and no_eth.calls == []
+
+
+def test_the_sender_check_is_case_insensitive_for_evm_and_exact_for_solana():
+ """EVM addresses are hex (checksum casing is cosmetic); Solana addresses are base58, where a
+ case change is a different account entirely."""
+ b = FakeBridge()
+ b.ethereum.address = b.eth.address = "0xAbC0000000000000000000000000000000000001"
+ execute(b, _wbtc_plan(b, sender="0xabc0000000000000000000000000000000000001"))
+ assert b.calls[0][0] == "eth.transfer_remote"
+
+ mixed = "So11111111111111111111111111111111111111112"
+ s = FakeBridge(solana=True)
+ s.solana.address = s.sol.address = mixed
+ with pytest.raises(ConfigurationError, match="sender"):
+ execute(s, _sol_plan(s, sender=mixed.lower()))
+ assert s.calls == []
+ execute(s, _sol_plan(s, sender=mixed))
+ assert s.calls[0][0] == "sol.transfer_remote"
+
+
+# ── the bound checkpoint store ────────────────────────────────────────────────
+
+def test_bound_store_saves_every_checkpoint_and_replaces_superseded_ids(tmp_path):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(checkpoints=store)
+ plan = _usdc_plan(b)
+ approval = Receipt(id="0x" + "11" * 32, protocol="xreserve", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": ["0x" + "11" * 32]})
+ b.eth.intermediates = [approval]
+ execute(b, plan)
+ saved = store.list()
+ assert [c.id for c in saved] == ["0x" + "cc" * 32] # approval file replaced by the deposit's
+ assert saved[0].source == {"transactionId": "0x" + "bb" * 32, "hookData": "0x" + "00" * 65}
+
+
+def test_module_and_lifecycle_checkpoint_channels_do_not_double_write(tmp_path):
+ """The module saves the checkpoints it emits and ``execute`` emits the final receipt again:
+ the caller still sees each boundary once and the store ends with one record per transfer."""
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(checkpoints=store)
+ plan = _wbtc_plan(b, sender=EVM_ADDRESS)
+ b.eth.intermediates = [Receipt(id="0x" + "11" * 32, protocol="hyperlane",
+ status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id,
+ "approvalTxIds": ["0x" + "11" * 32]})]
+ cps = []
+ execute(b, plan, on_checkpoint=cps.append)
+ assert [c.id for c in cps] == ["0x" + "11" * 32, "0x" + "aa" * 32] # no repeated boundary
+ saved = store.list()
+ assert [c.id for c in saved] == ["0x" + "aa" * 32]
+ assert store.load("0x" + "aa" * 32).source == {"transactionId": "0x" + "aa" * 32}
+
+
+def test_a_stale_plan_is_refused_before_anything_is_sent():
+ b = FakeBridge()
+ plan = dataclasses.replace(_wbtc_plan(b, sender=EVM_ADDRESS), registry_version="0000-00-00.stale")
+ with pytest.raises(Exception, match="registry"):
+ execute(b, plan)
+ assert b.calls == [] and b.events == []
+
+
+# ── review carry-overs (items 6-8) ────────────────────────────────────────────
+
+def test_persist_never_leaves_the_store_empty_between_checkpoints(tmp_path):
+ """Item 6: ``_persist`` saves the new checkpoint before deleting the superseded id, and a
+ module-emitted checkpoint's own supersede is deferred (parked on the ``_Emitter``) until the
+ module has actually saved it — otherwise a crash between delete and save loses the record."""
+ store = FileCheckpointStore(tmp_path)
+ observed = []
+ orig_save, orig_delete = store.save, store.delete
+
+ def save(cp):
+ orig_save(cp)
+ observed.append(len(store.list()))
+
+ def delete(cid):
+ orig_delete(cid)
+ observed.append(len(store.list()))
+
+ store.save, store.delete = save, delete
+ b = FakeBridge(checkpoints=store)
+ plan = _wbtc_plan(b, sender=EVM_ADDRESS)
+ b.eth.intermediates = [Receipt(id="0x" + "11" * 32, protocol="hyperlane",
+ status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id,
+ "approvalTxIds": ["0x" + "11" * 32]})]
+ execute(b, plan)
+ assert observed and all(n >= 1 for n in observed) # never empty in between
+ assert [c.id for c in store.list()] == ["0x" + "aa" * 32] # exactly one record after execute
+
+
+def test_aleo_hyperlane_leg_refuses_a_sender_mismatch_before_proving():
+ """Item 7: an Aleo leg checks the plan's sender against ``bridge.aleo_address()`` before
+ proving anything."""
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=EVM_ADDRESS,
+ sender="aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvfnl2t")
+ with pytest.raises(ConfigurationError, match="sender"):
+ execute(b, plan, gas_payment_microcredits=1)
+ assert b.calls == [] and "delegate_prepared" not in [e[0] for e in b.events]
+
+
+def test_aleo_xreserve_leg_refuses_a_sender_mismatch_before_proving():
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/usdcx", destination="ethereum/usdc", amount="2.5",
+ recipient=EVM_ADDRESS,
+ sender="aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqvfnl2t")
+ with pytest.raises(ConfigurationError, match="sender"):
+ execute(b, plan, mode="public")
+ assert b.calls == [] and "delegate_prepared" not in [e[0] for e in b.events]
+
+
+def test_aleo_leg_with_no_sender_pinned_and_no_configured_account_is_unaffected():
+ """A plan without a sender, or a bridge whose ``aleo_address()`` cannot be read, never blocks
+ the leg — the check is a no-op when there is nothing to compare."""
+ b = FakeBridge(ethereum=False)
+
+ def raises(*a, **kw):
+ raise ConfigurationError("no aleo account configured")
+
+ b.aleo_address = raises
+ execute(b, _aleo_eth_plan(b))
+ assert b.calls[0][0] == "hyperlane.quote_gas_payment"
+
+
+def test_destination_balance_baseline_read_is_best_effort(monkeypatch):
+ """Item 8: an advisory destination-balance read never blocks funds movement."""
+ b = FakeBridge()
+ b.eth.balances["ethereum/eth"] = 100
+
+ def raise_balance(asset):
+ raise RuntimeError("RPC is down")
+
+ monkeypatch.setattr(b.eth, "balance", raise_balance)
+ plan = _aleo_eth_plan(b)
+ cps = []
+ progress = execute(b, plan, gas_payment_microcredits=1, on_checkpoint=cps.append)
+ assert cps[0].delivery_verification is None
+ assert "destinationBalanceBeforeAtomic" not in progress.receipt.protocol_state
diff --git a/bridge-sdk/tests/test_freezelist.py b/bridge-sdk/tests/test_freezelist.py
new file mode 100644
index 0000000..8df793c
--- /dev/null
+++ b/bridge-sdk/tests/test_freezelist.py
@@ -0,0 +1,166 @@
+import pytest
+
+from aleo_bridge import freezelist as fl
+from aleo_bridge.errors import ConfigurationError
+
+# Vectors from sdk/src/integrations/sealance/merkle-tree.ts docstrings
+A = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px"
+B = "aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t"
+A_FIELD = 3501665755452795161867664882580888971213780722176652848275908626939553697821
+B_FIELD = 1295133970529764960316948294624974168921228814652993007266766481909235735940
+RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ZERO = fl.ZERO_ADDRESS
+PROGRAM = "usdcx_stablecoin.aleo" # the TOKEN program, as callers (privacy.py, xreserve.py) pass it
+FREEZE_LIST_PROGRAM = "usdcx_freezelist.aleo" # the program that actually holds the freeze list
+EMPTY_ONE = "{ siblings: [" + ", ".join(["0field"] * 16) + "], leaf_index: 1u32 }"
+
+
+def test_empty_pair_literal_matches_veil():
+ assert fl.EMPTY_MERKLE_PROOF_PAIR == f"[{EMPTY_ONE}, {EMPTY_ONE}]"
+ assert fl.EMPTY_MERKLE_PROOF_PAIR.count("0field") == 32 and fl.EMPTY_MERKLE_PROOF_PAIR.count("leaf_index: 1u32") == 2
+ assert fl.PROOF_SIBLINGS == 16 and fl.DEFAULT_DEPTH == 15
+
+
+def test_address_to_field_int_matches_ts_docstrings():
+ assert fl.address_to_field_int(A) == A_FIELD
+ assert fl.address_to_field_int(B) == B_FIELD
+ assert fl.address_to_field_int(ZERO) == 0
+
+
+def test_hash_two_uses_poseidon4_over_packed_array():
+ from aleo import mainnet as net
+ expected = str(net.Poseidon4().hash(net.Plaintext.from_string("[1field,3field,4field]").to_fields()))
+ assert fl.hash_two("1field", "3field", "4field", "mainnet") == expected
+ assert expected.endswith("field") and expected != "0field"
+ assert fl.hash_two("0field", "3field", "4field", "mainnet") != expected # prefix matters (leaf vs node)
+
+
+def test_empty_tree_root_pinned():
+ # Pinned 2026-09-03 from the live compliance tree ([0, 0, root]); investigate rather than re-pin on failure.
+ assert fl.build_tree(["0field", "0field"], "mainnet") == [0, 0, fl.EMPTY_TREE_ROOT]
+ assert fl.EMPTY_TREE_ROOT == 3642222252059314292809609689035560016959342421640560347114299934615987159853
+
+
+def test_generate_leaves_sorts_pads_and_filters_zero():
+ assert fl.generate_leaves([A, B, B]) == ["0field", f"{B_FIELD}field", f"{B_FIELD}field", f"{A_FIELD}field"]
+ assert fl.generate_leaves([ZERO, ZERO, A]) == ["0field", f"{A_FIELD}field"]
+ assert fl.generate_leaves([]) == ["0field", "0field"]
+ assert len(fl.generate_leaves([A] * 5)) == 8 and fl.generate_leaves([A] * 5)[:3] == ["0field"] * 3
+ with pytest.raises(ConfigurationError, match="Leaves limit exceeded"):
+ fl.generate_leaves([A] * (2 ** 14 + 1), 15)
+
+
+def test_build_tree_shapes_and_errors():
+ two = fl.build_tree(["1field", "2field"], "mainnet")
+ four = fl.build_tree(["1field", "2field", "3field", "4field"], "mainnet")
+ assert len(two) == 3 and two[:2] == [1, 2]
+ assert len(four) == 7 and four[:4] == [1, 2, 3, 4]
+ assert f"{four[4]}field" == fl.hash_two("1field", "1field", "2field", "mainnet") # leaf level uses 1field
+ assert f"{four[6]}field" == fl.hash_two("0field", f"{four[4]}field", f"{four[5]}field", "mainnet") # inner level uses 0field
+ with pytest.raises(ConfigurationError, match="cannot be empty"):
+ fl.build_tree([], "mainnet")
+ with pytest.raises(ConfigurationError, match="even number"):
+ fl.build_tree(["1field", "2field", "3field"], "mainnet")
+
+
+def test_leaf_indices_and_sibling_path():
+ tree = fl.build_tree(fl.generate_leaves([A]), "mainnet") # leaves [0, A_FIELD]
+ assert fl.leaf_indices(tree, RECIPIENT) == (1, 1) # RECIPIENT's field > A_FIELD → clamps to the last leaf
+ assert fl.leaf_indices(tree, A) == (0, 1) # equal to leaf 1 → (0, 1)
+ four = fl.build_tree(["1field", "2field", "3field", "4field"], "mainnet")
+ path = fl.sibling_path(four, 1, 15)
+ assert len(path) == 15 and path[:3] == [2, 1, four[5]] and path[3:] == [0] * 12 # leaf, sibling, uncle, zero-padding
+ assert len(fl.sibling_path(four, 1, 16)) == 16
+ assert fl.format_merkle_proof(([2, 1], 1), ([3, 4], 2)) == \
+ "[{ siblings: [2field, 1field], leaf_index: 1u32 }, { siblings: [3field, 4field], leaf_index: 2u32 }]"
+
+
+def test_pure_exclusion_proof_of_empty_tree_equals_veil_literal():
+ tree = fl.build_tree(["0field", "0field"], "mainnet")
+ left, right = fl.leaf_indices(tree, RECIPIENT)
+ assert (left, right) == (1, 1)
+ proof = fl.format_merkle_proof((fl.sibling_path(tree, left, fl.PROOF_SIBLINGS), left),
+ (fl.sibling_path(tree, right, fl.PROOF_SIBLINGS), right))
+ assert proof == fl.EMPTY_MERKLE_PROOF_PAIR
+
+
+def test_freezelist_reads_mappings_and_builds_proof(bridge):
+ mappings = bridge.aleo.mappings.setdefault(FREEZE_LIST_PROGRAM, {})
+ assert bridge.freezelist.leaves(PROGRAM) == []
+ # An unreadable on-chain root is fatal now (item 4): no silent fallback to an unverified proof.
+ with pytest.raises(ConfigurationError, match="unreadable"):
+ bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM)
+ mappings["freeze_list_root"] = {"1u8": f"{fl.EMPTY_TREE_ROOT}field"}
+ assert bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM) == fl.EMPTY_MERKLE_PROOF_PAIR
+ mappings["freeze_list_last_index"] = {"true": "1u32"}
+ mappings["freeze_list_index"] = {"0u32": A, "1u32": ZERO}
+ assert bridge.freezelist.leaves(PROGRAM) == [A] # zero address filtered
+ # The root moved once the list gained a member; seed the fake with the matching root.
+ mappings["freeze_list_root"] = {"1u8": f"{fl.build_tree(fl.generate_leaves([A]), 'mainnet')[-1]}field"}
+ assert bridge.freezelist.tree(PROGRAM)[:2] == [0, A_FIELD]
+ one = "{ siblings: [" + f"{A_FIELD}field, 0field, " + ", ".join(["0field"] * 14) + "], leaf_index: 1u32 }"
+ assert bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM) == f"[{one}, {one}]"
+ with pytest.raises(ConfigurationError, match="freeze list"):
+ bridge.freezelist.exclusion_proof(A, PROGRAM)
+
+
+def test_exclusion_proof_raises_when_root_unreadable(bridge):
+ # No usdcx_freezelist.aleo/freeze_list_root mapping at all — must raise, never fall back.
+ with pytest.raises(ConfigurationError, match=r"freeze_list_root\[1u8\] is unreadable"):
+ bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM)
+ with pytest.raises(ConfigurationError, match="unreadable"):
+ bridge.freezelist.tree(PROGRAM)
+
+
+def test_freezelist_last_index_parsing_is_not_rstrip(bridge):
+ mappings = bridge.aleo.mappings.setdefault(FREEZE_LIST_PROGRAM, {})
+ mappings["freeze_list_last_index"] = {"true": "12u32"}
+ mappings["freeze_list_index"] = {f"{i}u32": ZERO for i in range(12)} | {"12u32": B}
+ assert bridge.freezelist.leaves(PROGRAM) == [B]
+
+
+def test_freeze_list_program_resolves_via_imports(bridge):
+ bridge.aleo.imports["usdcx_stablecoin.aleo"] = ["credits.aleo", FREEZE_LIST_PROGRAM]
+ assert bridge.freezelist.freeze_list_program(PROGRAM) == FREEZE_LIST_PROGRAM
+
+
+def test_freeze_list_program_resolves_via_fallback_table_when_imports_unavailable(bridge):
+ # default fixture sets no imports for usdcx_stablecoin.aleo -> falls back to the static table
+ assert bridge.freezelist.freeze_list_program(PROGRAM) == FREEZE_LIST_PROGRAM
+ assert bridge.freezelist.freeze_list_program("test_usdcx_stablecoin.aleo") == "test_usdcx_freezelist.aleo"
+ assert bridge.freezelist.freeze_list_program(FREEZE_LIST_PROGRAM) == FREEZE_LIST_PROGRAM # already a freezelist program
+
+
+def test_freeze_list_program_raises_for_unknown_program(bridge):
+ with pytest.raises(ConfigurationError, match="no freeze-list program"):
+ bridge.freezelist.freeze_list_program("arc20_wbtc.aleo")
+
+
+def test_freeze_list_program_propagates_unrelated_exceptions(bridge, monkeypatch):
+ # Only ProgramNotFound/AleoError fall back to the static table; a transient failure of any
+ # other type (e.g. a network hiccup) must propagate, not be swallowed as "no imports".
+ def boom(program_id):
+ raise RuntimeError("rpc hiccup")
+
+ monkeypatch.setattr(bridge, "program", boom)
+ with pytest.raises(RuntimeError, match="rpc hiccup"):
+ bridge.freezelist.freeze_list_program(PROGRAM)
+
+
+def test_leaves_reads_from_freeze_list_program_not_token_program(bridge):
+ # the token program's own mappings are deliberately wrong, to prove they are never consulted
+ bridge.aleo.mappings[PROGRAM]["freeze_list_last_index"] = {"true": "0u32"}
+ bridge.aleo.mappings[PROGRAM]["freeze_list_index"] = {"0u32": A}
+ fl_mappings = bridge.aleo.mappings.setdefault(FREEZE_LIST_PROGRAM, {})
+ fl_mappings["freeze_list_last_index"] = {"true": "1u32"}
+ fl_mappings["freeze_list_index"] = {"0u32": ZERO, "1u32": B} # index 0 is the zero-address sentinel
+ assert bridge.freezelist.leaves(PROGRAM) == [B]
+
+
+def test_exclusion_proof_verifies_onchain_root(bridge):
+ fl_mappings = bridge.aleo.mappings.setdefault(FREEZE_LIST_PROGRAM, {})
+ fl_mappings["freeze_list_root"] = {"1u8": "123field"}
+ with pytest.raises(ConfigurationError, match="freeze-list root"):
+ bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM)
+ fl_mappings["freeze_list_root"] = {"1u8": f"{fl.EMPTY_TREE_ROOT}field"}
+ assert bridge.freezelist.exclusion_proof(RECIPIENT, PROGRAM) == fl.EMPTY_MERKLE_PROOF_PAIR
diff --git a/bridge-sdk/tests/test_gen_context.py b/bridge-sdk/tests/test_gen_context.py
new file mode 100644
index 0000000..033fef6
--- /dev/null
+++ b/bridge-sdk/tests/test_gen_context.py
@@ -0,0 +1,49 @@
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+GEN = ROOT / "codegen" / "gen_context.py"
+
+
+def _render() -> str:
+ out = subprocess.run([sys.executable, str(GEN), "--stdout"], capture_output=True, text=True, cwd=ROOT)
+ assert out.returncode == 0, out.stderr
+ return out.stdout
+
+
+def test_tier1_lifecycle_and_conversation_pattern():
+ page = _render()
+ for verb in ("from_env", "from_profile", "status", "quote", "execute", "wait", "recover", "resume",
+ "complete", "pending"):
+ assert f"### `{verb}(" in page, verb
+ assert "## Serving a chatting user" in page
+ assert "NEVER ask the user to paste a private key" in page
+ assert "quote first" in page.lower() and "human units" in page.lower()
+ for nxt in ("`wait`", "`resume`", "`complete`", "`done`", "`failed`"):
+ assert nxt in page # progress.next table
+ assert "irreversible" in page.lower()
+ assert "secret_nonce" in page and "never stores" in page.lower()
+
+
+def test_tier2_modules_and_registry_table():
+ page = _render()
+ for method in ("hyperlane.transfer_remote", "hyperlane.quote_gas_payment", "xreserve.burn",
+ "xreserve.private_mint", "xreserve.get_attestation", "shield", "unshield",
+ "freezelist.exclusion_proof", "eth.transfer_remote", "eth.deposit_usdc",
+ "eth.quote_transfer_remote", "sol.transfer_remote", "sol.quote_transfer_remote"):
+ assert f"### `{method}(" in page, method
+ from aleo_bridge.registry import DEFAULT_REGISTRY
+ for route in DEFAULT_REGISTRY.routes(include_unavailable=True, environment=None):
+ assert f"`{route.id}`" in page, route.id
+ assert "| metadata-required |" in page and "| active |" in page
+
+
+def test_committed_pages_are_current():
+ check = subprocess.run([sys.executable, str(GEN), "--check"], capture_output=True, text=True, cwd=ROOT)
+ assert check.returncode == 0, check.stderr or "AGENTS.md stale — run codegen/gen_context.py"
+ assert (ROOT / "AGENTS.md").read_text() == (ROOT / "python" / "aleo_bridge" / "AGENTS.md").read_text()
+
+
+def test_page_stays_compact():
+ assert len(_render()) < 40_000
diff --git a/bridge-sdk/tests/test_get_status.py b/bridge-sdk/tests/test_get_status.py
new file mode 100644
index 0000000..fb847ae
--- /dev/null
+++ b/bridge-sdk/tests/test_get_status.py
@@ -0,0 +1,291 @@
+import pytest
+
+from aleo_bridge.encoding import (xreserve_deposit_payload, xreserve_hook_data, xreserve_message_hash,
+ xreserve_nonce_from_payload)
+from aleo_bridge.errors import (BridgeError, CheckpointInvalidError, ConfigurationError,
+ DeliveryUnknownError, UnsupportedRouteError)
+from aleo_bridge.lifecycle import aleo_transaction_status, get_status, prepare
+from aleo_bridge.types import Attestation, Receipt, Status
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+
+SIG = "0x" + "11" * 65
+
+
+def _inbound_private(b):
+ plan = prepare(b.registry, source="sepolia/usdc", destination="aleo-testnet/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, mint_mode="private")
+ hook = xreserve_hook_data("private", ALEO_RECIPIENT, "testnet", "7scalar")
+ payload = xreserve_deposit_payload(amount=2_000_000, remote_domain=10_002, remote_token=b"\x11" * 32,
+ remote_recipient=b"\x22" * 32,
+ local_token="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
+ depositor=EVM_ADDRESS, max_fee=100_000, nonce=b"\x00" * 32, hook_data=hook)
+ message_hash = "0x" + xreserve_message_hash(payload).hex()
+ receipt = Receipt(id=message_hash, protocol="xreserve", status=Status.ATTESTATION_PENDING,
+ source_tx_id="0x" + "22" * 32,
+ protocol_state={"routeId": plan.route_id, "mintMode": "private",
+ "intendedRecipient": ALEO_RECIPIENT, "payload": "0x" + payload.hex(),
+ "messageHash": message_hash, "bridgeProgram": "test_usdcx_bridge_v2.aleo"})
+ return plan, payload, message_hash, receipt
+
+
+def test_guards_and_terminal_passthrough():
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/eth", destination="ethereum/eth", amount="0.000000000000000001",
+ recipient=EVM_ADDRESS)
+ with pytest.raises(CheckpointInvalidError, match="does not match"):
+ get_status(b, plan, Receipt(id="x", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": "other"}))
+ done = Receipt(id="x", protocol="hyperlane", status=Status.COMPLETED, protocol_state={"routeId": plan.route_id})
+ assert get_status(b, plan, done) is done
+ failed = done.replace(status=Status.FAILED)
+ assert get_status(b, plan, failed) is failed and b.calls == [] and b.events == []
+ expired = done.replace(status=Status.EXPIRED)
+ assert get_status(b, plan, expired) is expired and b.calls == [] and b.events == []
+
+
+def test_branch1_evm_approval_pending_delegates_to_eth_source_status():
+ b = FakeBridge()
+ plan = prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001", recipient=ALEO_RECIPIENT)
+ receipt = Receipt(id="0x" + "11" * 32, protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": ["0x" + "11" * 32]})
+ b.eth.source_status_result = receipt.replace(status=Status.SOURCE_SUBMISSION_PENDING)
+ out = get_status(b, plan, receipt)
+ assert out.status is Status.SOURCE_SUBMISSION_PENDING and b.calls == [("eth.source_status", Status.SOURCE_APPROVAL_PENDING)]
+
+
+@pytest.mark.parametrize("node_status,expected", [("accepted", Status.DELIVERY_PENDING), ("rejected", Status.FAILED)])
+def test_branch2_aleo_source_confirming_reads_confirmed_transaction(node_status, expected):
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/usdcx", destination="ethereum/usdc", amount="2.1", recipient=EVM_ADDRESS)
+ receipt = Receipt(id="at1burn", protocol="xreserve", status=Status.SOURCE_CONFIRMING, source_tx_id="at1burn",
+ protocol_state={"routeId": plan.route_id}, next_action={"kind": "stale"})
+ pending = get_status(b, plan, receipt)
+ assert pending is receipt # TransactionNotFound → unchanged
+ b.aleo.confirmed_transactions["at1burn"] = {"status": node_status, "type": "execute", "index": 3,
+ "transaction": {"id": "at1burn"}, "finalize": []}
+ out = get_status(b, plan, receipt)
+ assert out.status is expected and out.next_action is None
+ if expected is Status.FAILED:
+ assert "rejected" in out.protocol_state["sourceError"]
+ assert aleo_transaction_status(b, "at1burn")[0] == node_status
+ assert aleo_transaction_status(b, "at1unknown") == ("pending", None)
+
+
+def test_branch3_4_hyperlane_source_confirming_evm_and_solana():
+ b = FakeBridge(solana=True)
+ evm_plan = prepare(b.registry, source="ethereum/eth", destination="aleo/eth", amount="0.000000000000000001",
+ recipient=ALEO_RECIPIENT)
+ evm_receipt = Receipt(id="0x" + "aa" * 32, protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="0x" + "aa" * 32, protocol_state={"routeId": evm_plan.route_id})
+ b.eth.source_status_result = evm_receipt.replace(status=Status.DELIVERY_PENDING,
+ protocol_state={**evm_receipt.protocol_state, "messageId": "0x" + "cd" * 32})
+ assert get_status(b, evm_plan, evm_receipt).protocol_state["messageId"] == "0x" + "cd" * 32
+ sol_plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ sol_receipt = Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id="sig",
+ protocol_state={"routeId": sol_plan.route_id})
+ b.sol.source_status_result = sol_receipt.replace(status=Status.EXPIRED,
+ protocol_state={**sol_receipt.protocol_state, "blockhashExpired": True,
+ "sourceError": "Solana transaction expired before confirmation: sig"})
+ assert get_status(b, sol_plan, sol_receipt).status is Status.EXPIRED
+ assert b.calls == [("eth.source_status", Status.SOURCE_CONFIRMING), ("sol.source_status", Status.SOURCE_CONFIRMING)]
+
+
+def test_branch5_hyperlane_delivery_via_destination_mailbox():
+ b = FakeBridge()
+ to_aleo = prepare(b.registry, source="ethereum/eth", destination="aleo/eth", amount="0.000000000000000001",
+ recipient=ALEO_RECIPIENT)
+ mid = "0x" + "cd" * 32
+ receipt = Receipt(id=mid, protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id="0x" + "aa" * 32,
+ protocol_state={"routeId": to_aleo.route_id, "messageId": mid})
+ assert get_status(b, to_aleo, receipt) is receipt
+ b.hyperlane.delivered[mid] = True
+ assert get_status(b, to_aleo, receipt).status is Status.COMPLETED
+ assert b.calls[-1] == ("hyperlane.is_delivered", mid)
+
+ to_evm = prepare(b.registry, source="aleo/eth", destination="ethereum/eth", amount="0.000000000000000001",
+ recipient=EVM_ADDRESS)
+ receipt2 = Receipt(id="at1x", protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id="at1x",
+ protocol_state={"routeId": to_evm.route_id, "messageId": mid})
+ b.eth.delivered[mid] = True
+ assert get_status(b, to_evm, receipt2).status is Status.COMPLETED
+ assert b.calls[-1] == ("eth.is_delivered", mid)
+
+
+def test_branch5_solana_delivery_pending_without_message_id_fills_from_logs():
+ b = FakeBridge(solana=True)
+ plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ sig = "5igNature" * 8
+ receipt = Receipt(id=sig, protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id=sig,
+ protocol_state={"routeId": plan.route_id, "messageIdUnavailable": True})
+
+ # the log read raises -> unchanged, never falls through to is_delivered with the signature
+ b.sol.transaction_logs_error = RuntimeError("solana RPC is down")
+ assert get_status(b, plan, receipt) is receipt
+ assert not any(call[0] == "hyperlane.is_delivered" for call in b.calls)
+ b.sol.transaction_logs_error = None
+
+ # the log read succeeds but carries no dispatch line -> still unavailable, unchanged
+ b.sol.transaction_logs_result = ["Program log: something unrelated"]
+ assert get_status(b, plan, receipt) is receipt
+ assert not any(call[0] == "hyperlane.is_delivered" for call in b.calls)
+
+ # the dispatch line is present -> message id filled in, then the destination Mailbox is checked
+ mid = "0x" + "cd" * 32
+ b.sol.transaction_logs_result = [f"Program log: Dispatched message to 1399811149, ID {mid}"]
+ out = get_status(b, plan, receipt)
+ assert out.id == mid
+ assert out.protocol_state["messageId"] == mid
+ assert "messageIdUnavailable" not in out.protocol_state
+ assert out.status is Status.DELIVERY_PENDING
+ assert b.calls[-1] == ("hyperlane.is_delivered", mid)
+
+ b.hyperlane.delivered[mid] = True
+ assert get_status(b, plan, receipt).status is Status.COMPLETED
+
+
+def test_branch6_aleo_origin_balance_diff_fallback():
+ b = FakeBridge(solana=True)
+ plan = prepare(b.registry, source="aleo/sol", destination="solana/sol", amount="0.000000001", recipient=SOL_ADDRESS)
+ receipt = Receipt(id="at1source", protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id="at1source",
+ protocol_state={"routeId": plan.route_id, "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"})
+ b.sol.balance_lamports = 100
+ assert get_status(b, plan, receipt) is receipt
+ b.sol.balance_lamports = 101
+ assert get_status(b, plan, receipt).status is Status.COMPLETED
+ no_baseline = receipt.replace(protocol_state={"routeId": plan.route_id})
+ assert get_status(b, plan, no_baseline) is no_baseline # branch 7: unchanged
+ b2 = FakeBridge(ethereum=False)
+ with pytest.raises(DeliveryUnknownError, match="destination balance"):
+ get_status(b2, plan, receipt)
+
+
+def test_branch6_a_failing_destination_balance_read_propagates():
+ """Carried from the Task 6 review (item 8): in branch 6 the destination balance is the DELIVERY
+ SIGNAL, not an advisory baseline. A transport failure must surface here so ``wait``'s transient
+ classifier can retry it — swallowing it would read as "not delivered yet" forever."""
+ b = FakeBridge(solana=True)
+ plan = prepare(b.registry, source="aleo/sol", destination="solana/sol", amount="0.000000001",
+ recipient=SOL_ADDRESS)
+ receipt = Receipt(id="at1source", protocol="hyperlane", status=Status.DELIVERY_PENDING,
+ source_tx_id="at1source",
+ protocol_state={"routeId": plan.route_id, "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"})
+
+ def boom():
+ raise BridgeError("Solana RPC request failed with HTTP status 429")
+
+ b.sol.balance = boom
+ with pytest.raises(BridgeError, match="429"):
+ get_status(b, plan, receipt)
+
+
+def test_branch8_and_9_xreserve_outbound_and_not_implemented():
+ b = FakeBridge(ethereum=False)
+ plan = prepare(b.registry, source="aleo/usdcx", destination="ethereum/usdc", amount="2.1", recipient=EVM_ADDRESS)
+ receipt = Receipt(id="at1burn", protocol="xreserve", status=Status.DELIVERY_PENDING, source_tx_id="at1burn",
+ protocol_state={"routeId": plan.route_id})
+ assert get_status(b, plan, receipt) is receipt
+ with pytest.raises(UnsupportedRouteError, match="not implemented"):
+ get_status(b, plan, receipt.replace(status=Status.ATTESTATION_PENDING))
+
+
+def test_branch10_nullifier_first_then_attestation_then_private_action():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ # nullifier read comes first for every inbound pending status, and derives the nonce from the payload
+ b.xreserve.delivered_nonces.add("0x" + xreserve_nonce_from_payload(payload).hex())
+ waiting = receipt.replace(status=Status.DESTINATION_ACTION_REQUIRED,
+ next_action={"kind": "xreserve-private-mint", "chainId": "aleo-testnet"})
+ out = get_status(b, plan, waiting)
+ assert out.status is Status.COMPLETED and out.next_action is None
+ assert b.calls[0][0] == "xreserve.is_delivered"
+ b.xreserve.delivered_nonces.clear()
+ # stored nonce wins over the payload
+ stored = receipt.replace(status=Status.DELIVERY_PENDING,
+ protocol_state={**receipt.protocol_state, "nonce": "0x" + "33" * 32})
+ b.xreserve.delivered_nonces.add("0x" + "33" * 32)
+ assert get_status(b, plan, stored).status is Status.COMPLETED
+ b.xreserve.delivered_nonces.clear()
+ # attestation pending → unchanged; complete + private → DESTINATION_ACTION_REQUIRED
+ assert get_status(b, plan, receipt) is receipt
+ b.xreserve.attestations[message_hash] = Attestation(payload=payload, message_hash=bytes.fromhex(message_hash[2:]),
+ attestation=bytes.fromhex(SIG[2:]), status="complete")
+ ready = get_status(b, plan, receipt)
+ assert ready.status is Status.DESTINATION_ACTION_REQUIRED
+ assert ready.next_action == {"kind": "xreserve-private-mint", "chainId": "aleo-testnet"}
+ assert ready.protocol_state["attestation"] == SIG
+ assert get_status(b, plan, ready) is ready # action required → unchanged
+ # public mode → DELIVERY_PENDING with attestation kept
+ public_plan = prepare(b.registry, source="sepolia/usdc", destination="aleo-testnet/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT)
+ public = receipt.replace(protocol_state={**receipt.protocol_state, "mintMode": "public"})
+ out = get_status(b, public_plan, public)
+ assert out.status is Status.DELIVERY_PENDING and out.protocol_state["attestation"] == SIG
+ with pytest.raises(CheckpointInvalidError, match="message hash"):
+ get_status(b, plan, receipt.replace(protocol_state={"routeId": plan.route_id}))
+
+
+def test_branch10_source_confirming_and_destination_confirming():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ confirming = receipt.replace(status=Status.SOURCE_CONFIRMING)
+ b.eth.source_status_result = receipt
+ assert get_status(b, plan, confirming) is receipt and b.calls[-1][0] == "eth.source_status"
+ minting = receipt.replace(status=Status.DESTINATION_CONFIRMING, destination_tx_id="at1private",
+ protocol_state={**receipt.protocol_state, "attestation": SIG})
+ assert get_status(b, plan, minting) is minting
+ b.aleo.confirmed_transactions["at1private"] = {"status": "accepted", "type": "execute"}
+ assert get_status(b, plan, minting).status is Status.COMPLETED
+ b.aleo.confirmed_transactions["at1private"] = {"status": "rejected", "type": "execute", "rejected": {"type": "execution"}}
+ out = get_status(b, plan, minting)
+ assert out.status is Status.FAILED and "at1private" in out.protocol_state["destinationError"]
+ with pytest.raises(CheckpointInvalidError, match="destination transaction id"):
+ get_status(b, plan, minting.replace(destination_tx_id=None))
+
+
+def test_branch10_delivered_wins_over_attestation_pending():
+ # Carried from the Task 5 review (item 7): the destination nullifier check (invariant 6) must
+ # run BEFORE the attestation read for EVERY inbound-pending status, including
+ # ATTESTATION_PENDING itself, not just DESTINATION_ACTION_REQUIRED/DELIVERY_PENDING — so a
+ # nonce that is already delivered short-circuits straight to COMPLETED even when a complete
+ # attestation is also scripted, and xreserve.get_attestation is never called.
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ nonce = "0x" + xreserve_nonce_from_payload(payload).hex()
+ b.xreserve.delivered_nonces.add(nonce)
+ b.xreserve.attestations[message_hash] = Attestation(payload=payload, message_hash=bytes.fromhex(message_hash[2:]),
+ attestation=bytes.fromhex(SIG[2:]), status="complete")
+ out = get_status(b, plan, receipt)
+ assert out.status is Status.COMPLETED
+ assert not any(call[0] == "xreserve.get_attestation" for call in b.calls)
+
+
+def test_message_id_never_falls_back_to_source_tx_id():
+ # Carried from the Task 5 review (item 8): a DispatchId log that could not be read leaves
+ # receipt.id == receipt.source_tx_id (the source transaction hash) — that must never be
+ # mistaken for the Hyperlane message id, even though it has the same 0x + 64-hex shape.
+ b = FakeBridge()
+ to_aleo = prepare(b.registry, source="ethereum/eth", destination="aleo/eth", amount="0.000000000000000001",
+ recipient=ALEO_RECIPIENT)
+ tx = "0x" + "aa" * 32
+ receipt = Receipt(id=tx, protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id=tx,
+ protocol_state={"routeId": to_aleo.route_id})
+ assert get_status(b, to_aleo, receipt) is receipt
+ assert not any(call[0] == "hyperlane.is_delivered" for call in b.calls)
+
+
+def test_solana_message_id_fill_in_raises_on_missing_connection():
+ # Carried from the Task 5 review (item 9): resolving the Solana connection must happen OUTSIDE
+ # the log-read try/except, so a missing connection surfaces as ConfigurationError instead of
+ # being swallowed as "still unavailable".
+ b = FakeBridge(ethereum=False, solana=False)
+ plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ sig = "5igNature" * 8
+ receipt = Receipt(id=sig, protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id=sig,
+ protocol_state={"routeId": plan.route_id, "messageIdUnavailable": True})
+ with pytest.raises(ConfigurationError, match="Solana"):
+ get_status(b, plan, receipt)
diff --git a/bridge-sdk/tests/test_hyperlane.py b/bridge-sdk/tests/test_hyperlane.py
new file mode 100644
index 0000000..5702e10
--- /dev/null
+++ b/bridge-sdk/tests/test_hyperlane.py
@@ -0,0 +1,204 @@
+import pytest
+
+from aleo_bridge import hyperlane as hl
+from aleo_bridge.errors import (AmbiguousRouteError, ConfigurationError, InvalidAmountError, InvalidRecipientError,
+ RouteUnavailableError, UnsupportedRouteError)
+from aleo_bridge.registry import DEFAULT_REGISTRY as REG
+from aleo_bridge.types import DispatchReceipt, Status
+from tests.conftest import ETH_GAS_CONFIG, IGP_KEY_ETH
+
+EVM1 = "0x0000000000000000000000000000000000000001"
+SOL_SYSTEM = "11111111111111111111111111111111"
+ZERO = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
+MAILBOX_STATE = ("{ default_hook: aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74, "
+ "required_hook: aleo1yxevh9qgxehej46j7vueplwjcpfdfml2dje3ey4ukzknx7wzasgqnxgq82 }")
+ROUTES = [ # veil test/actions/aleoHyperlane.test.ts ROUTES (active four); amount "1" in source decimals
+ ("aleo/eth", "hyperlane:aleo/eth->ethereum/eth", "hyp_warp_token_eth_v2.aleo", EVM1, 10**18),
+ ("aleo/wbtc", "hyperlane:aleo/wbtc->ethereum/wbtc", "hyp_warp_token_wbtc_v2.aleo", EVM1, 10**8),
+ ("aleo/usdt", "hyperlane:aleo/usdt->ethereum/usdt", "hyp_warp_token_usdt_v2.aleo", EVM1, 10**6),
+ ("aleo/sol", "hyperlane:aleo/sol->solana/sol", "hyp_warp_token_sol_v2.aleo", SOL_SYSTEM, 10**9),
+]
+
+
+def _inputs(bridge, route_id, recipient, amount, gas=8174147):
+ route = REG.route(route_id)
+ src, dst = REG.asset(route.source_asset_id), REG.asset(route.destination_asset_id)
+ return bridge.hyperlane.build_transfer_remote_inputs(
+ route, recipient=recipient, amount_atomic=amount, gas_payment_microcredits=gas,
+ decimals=(route.metadata["aleoLocalDecimals"], route.metadata["aleoRemoteDecimals"]))
+
+
+def test_pure_helpers():
+ assert hl.parse_gas_config(ETH_GAS_CONFIG) == {"gas_overhead": 159337, "exchange_rate": 402, "gas_price": 1000000000}
+ with pytest.raises(ConfigurationError, match="malformed"):
+ hl.parse_gas_config("{ gas_overhead: 1u128 }")
+ assert hl.gas_config_key(REG.route("hyperlane:aleo/eth->ethereum/eth")) == IGP_KEY_ETH
+ assert hl.compute_gas_payment(gas_limit=44000, gas_overhead=159337, gas_price=1000000000, exchange_rate=402) == 8174147
+ with pytest.raises(ConfigurationError, match="positive u64"):
+ hl.compute_gas_payment(gas_limit=0, gas_overhead=0, gas_price=1, exchange_rate=1)
+ with pytest.raises(ConfigurationError, match="positive u64"):
+ hl.compute_gas_payment(gas_limit=1, gas_overhead=0, gas_price=2**128 - 1, exchange_rate=2**128 - 1)
+
+
+@pytest.mark.parametrize("asset,route_id,program,recipient,amount", ROUTES)
+def test_outbound_route_and_common_shape(bridge, asset, route_id, program, recipient, amount):
+ route = bridge.hyperlane.outbound_route(asset)
+ assert route.id == route_id and route.meta_str("aleoRouterProgram") == program
+ inputs = _inputs(bridge, route_id, recipient, amount, gas=1)
+ assert len(inputs) == 7
+ assert inputs[1] == MAILBOX_STATE
+ assert inputs[6].count("spender:") == 4 and inputs[6].count("amount: 0u64") == 3 and "amount: 1u64" in inputs[6]
+ assert inputs[6].startswith("[{ spender: aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74, amount: 1u64 }, { spender: " + ZERO)
+ assert inputs[5] == f"{amount}u128"
+
+
+def test_eth_inputs_match_veil_vector(bridge):
+ inputs = _inputs(bridge, "hyperlane:aleo/eth->ethereum/eth", EVM1, 10**18)
+ assert inputs[0] == ("{ token_type: 1u8, token_owner: aleo1wq6f6qdqya44avznygz5hae40u3mjg64w0r93a4qfu4utpf8cg9q566f4r, "
+ "ism: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "hook: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "token_id: 133188123661477349522757068766864658505569365361420630212878794317749195359field, "
+ "local_decimals: 18u8, remote_decimals: 18u8 }")
+ assert inputs[2] == ("{ domain: 1u32, recipient: [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, "
+ "56u8, 212u8, 71u8, 105u8, 79u8, 92u8, 31u8, 119u8, 58u8, 227u8, 19u8, 44u8, 249u8, 59u8, "
+ "243u8, 11u8, 126u8, 193u8, 250u8, 90u8], gas: 44000u128 }")
+ assert inputs[3] == "1u32"
+ assert inputs[4] == "[0u128, 1329227995784915872903807060280344576u128]"
+ assert inputs[5] == "1000000000000000000u128"
+ assert inputs[6] == ("[{ spender: aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74, amount: 8174147u64 }, "
+ f"{{ spender: {ZERO}, amount: 0u64 }}, {{ spender: {ZERO}, amount: 0u64 }}, {{ spender: {ZERO}, amount: 0u64 }}]")
+
+
+def test_wbtc_inputs_match_veil_vector(bridge):
+ inputs = _inputs(bridge, "hyperlane:aleo/wbtc->ethereum/wbtc", EVM1, 10**8)
+ assert inputs[0] == ("{ token_type: 1u8, token_owner: aleo14jauje2a5sncm9u5t3mt6qqv3eq2hatkddskccs0dvsy35a0x58q0d6f95, "
+ "ism: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "hook: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "token_id: 1505227928464760254508513036497943623956572091841806589002910775534260084309field, "
+ "local_decimals: 8u8, remote_decimals: 8u8 }")
+ assert inputs[2] == ("{ domain: 1u32, recipient: [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, "
+ "32u8, 205u8, 200u8, 87u8, 120u8, 183u8, 50u8, 7u8, 63u8, 126u8, 236u8, 239u8, 61u8, 242u8, "
+ "92u8, 13u8, 49u8, 15u8, 135u8, 114u8], gas: 68000u128 }")
+ assert inputs[5] == "100000000u128"
+
+
+def test_usdt_inputs_match_veil_vector(bridge):
+ inputs = _inputs(bridge, "hyperlane:aleo/usdt->ethereum/usdt", EVM1, 10**6)
+ assert inputs[0] == ("{ token_type: 1u8, token_owner: aleo1l3gwacmjruxryy9c7c4fn0acyzprf29hucrvthw7f63lpyhd5y9srydq8z, "
+ "ism: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "hook: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "token_id: 8295938150000417034830036849466229528602563851235385582732969109393809606969field, "
+ "local_decimals: 6u8, remote_decimals: 18u8 }")
+ assert inputs[2] == ("{ domain: 1u32, recipient: [0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, "
+ "60u8, 32u8, 100u8, 215u8, 142u8, 69u8, 120u8, 232u8, 249u8, 54u8, 227u8, 219u8, 66u8, 174u8, "
+ "240u8, 68u8, 227u8, 63u8, 191u8, 49u8], gas: 68000u128 }")
+ assert inputs[5] == "1000000u128"
+
+
+def test_sol_inputs_match_veil_vector(bridge):
+ inputs = _inputs(bridge, "hyperlane:aleo/sol->solana/sol", SOL_SYSTEM, 10**9)
+ assert inputs[0] == ("{ token_type: 1u8, token_owner: aleo1wr8rfr4ggedjxtg5e23s38zqkgy2j05uc9l8t4akjp5zcw3levpswkwk45, "
+ "ism: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "hook: aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc, "
+ "token_id: 6148061383892805373029428966764338809222769879628268522058032128225601478383field, "
+ "local_decimals: 9u8, remote_decimals: 9u8 }")
+ assert inputs[2] == ("{ domain: 1399811149u32, recipient: [112u8, 4u8, 72u8, 22u8, 219u8, 143u8, 68u8, 202u8, "
+ "21u8, 197u8, 236u8, 182u8, 198u8, 142u8, 52u8, 96u8, 142u8, 38u8, 51u8, 113u8, 116u8, "
+ "143u8, 96u8, 123u8, 104u8, 126u8, 97u8, 73u8, 7u8, 6u8, 211u8, 122u8], gas: 300000u128 }")
+ assert inputs[3] == "1399811149u32" and inputs[4] == "[0u128, 0u128]" and inputs[5] == "1000000000u128"
+ real = _inputs(bridge, "hyperlane:aleo/sol->solana/sol", "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7", 1)
+ assert real[4] == "[127878782877948140186055645953777992816u128, 163261512394675613100746600600636171918u128]"
+
+
+def test_placeholder_usad_route_is_inspectable_but_not_executable(bridge):
+ usad = REG.route("hyperlane:aleo/usad->ethereum/usad")
+ inputs = bridge.hyperlane.build_transfer_remote_inputs(usad, recipient=EVM1, amount_atomic=1_000_000,
+ gas_payment_microcredits=1, decimals=(6, 6))
+ assert inputs[3] == "1u32" and inputs[4] == "[0u128, 1329227995784915872903807060280344576u128]" and inputs[5] == "1000000u128"
+ assert "gas: 0u128" in inputs[2] and inputs[0].startswith("{ token_type: 0u8, token_owner: aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n")
+ with pytest.raises(RouteUnavailableError, match="not executable"):
+ bridge.hyperlane.outbound_route("aleo/usad")
+ with pytest.raises(RouteUnavailableError):
+ bridge.hyperlane.transfer_remote("aleo/usad", EVM1, amount="1")
+ with pytest.raises(RouteUnavailableError): # four metadata-required ALEO routes, none active
+ bridge.hyperlane.outbound_route("aleo/aleo")
+ with pytest.raises(UnsupportedRouteError, match="Aleo asset"):
+ bridge.hyperlane.outbound_route("ethereum/eth")
+ assert bridge.aleo.calls == []
+
+
+def test_input_validation(bridge):
+ route = REG.route("hyperlane:aleo/eth->ethereum/eth")
+ kw = dict(recipient=EVM1, amount_atomic=1, gas_payment_microcredits=1, decimals=(18, 18))
+ for bad in (0, 1 << 64):
+ with pytest.raises(ConfigurationError, match="positive u64"):
+ bridge.hyperlane.build_transfer_remote_inputs(route, **{**kw, "gas_payment_microcredits": bad})
+ with pytest.raises(InvalidAmountError, match="greater than zero"):
+ bridge.hyperlane.build_transfer_remote_inputs(route, **{**kw, "amount_atomic": 0})
+ with pytest.raises(InvalidRecipientError, match="ethereum address format"):
+ bridge.hyperlane.build_transfer_remote_inputs(route, **{**kw, "recipient": "aleo1" + "a" * 58})
+ with pytest.raises(InvalidRecipientError):
+ bridge.hyperlane.build_transfer_remote_inputs(REG.route("hyperlane:aleo/sol->solana/sol"), **{**kw, "recipient": EVM1, "decimals": (9, 9)})
+ with pytest.raises(InvalidAmountError):
+ bridge.hyperlane.transfer_remote("aleo/eth", EVM1, amount="0.1234567890123456789") # 19 fractional digits
+
+
+def test_quote_gas_payment_vector(bridge):
+ quote = bridge.hyperlane.quote_gas_payment("aleo/eth")
+ assert (quote.route_id, quote.gas_limit, quote.gas_overhead, quote.gas_price, quote.exchange_rate, quote.payment_microcredits) == \
+ ("hyperlane:aleo/eth->ethereum/eth", 44000, 159337, 1000000000, 402, 8174147)
+ assert bridge.aleo.fetched.count("hyp_hook_manager.aleo") >= 1
+ sol = bridge.hyperlane.quote_gas_payment("aleo/sol") # SOL_GAS_CONFIG: (300000+200000)*50000000*1000 // 10**10
+ assert (sol.gas_limit, sol.payment_microcredits) == (300000, 2_500_000)
+
+
+def test_quote_gas_payment_failure_modes(bridge):
+ configs = bridge.aleo.mappings["hyp_hook_manager.aleo"]["destination_gas_configs"]
+ configs[IGP_KEY_ETH] = "{ gas_overhead: 0u128, exchange_rate: 0u128, gas_price: 0u128 }"
+ with pytest.raises(ConfigurationError, match="unpriced"):
+ bridge.hyperlane.quote_gas_payment("aleo/eth")
+ del configs[IGP_KEY_ETH]
+ with pytest.raises(ConfigurationError, match="missing on chain"):
+ bridge.hyperlane.quote_gas_payment("aleo/eth")
+ with pytest.raises(UnsupportedRouteError):
+ bridge.hyperlane.quote_gas_payment("ethereum/eth")
+
+
+def test_zero_gas_limit_falls_back_to_50000(bridge):
+ from dataclasses import replace
+ route = REG.route("hyperlane:aleo/eth->ethereum/eth")
+ zero = replace(route, metadata={**route.metadata, "aleoRemoteRouterGas": "0"})
+ quote = bridge.hyperlane.quote_gas_payment(zero)
+ assert quote.gas_limit == 50_000 and quote.payment_microcredits == (50_000 + 159337) * 1000000000 * 402 // 10**10
+
+
+def test_transfer_remote_builds_call_with_live_quote(bridge):
+ # WBTC shares the Ethereum IGP config (same destination domain 1u32) but carries its own
+ # aleoRemoteRouterGas (68000, vs. ETH's 44000); per compute_gas_payment the live quote is
+ # (68000 + 159337) * 1_000_000_000 * 402 // 10_000_000_000 == 9138947, not ETH's 8174147.
+ call = bridge.hyperlane.transfer_remote("aleo/wbtc", EVM1, amount="0.0001", as_signer=True)
+ assert (call.program_id, call.function_name) == ("hyp_warp_token_wbtc_v2.aleo", "transfer_remote_as_signer")
+ assert call.inputs[5] == "10000u128" and "amount: 9138947u64" in call.inputs[6]
+ assert bridge.aleo.submitted == [] # nothing sent until a verb runs
+ result = call.delegate(wait=False)
+ assert isinstance(result, DispatchReceipt)
+ assert (result.transaction_id, result.route_id, result.message_id, result.amount_atomic) == ("at1delegated", "hyperlane:aleo/wbtc->ethereum/wbtc", None, 10_000)
+ assert result.receipt.status is Status.SOURCE_CONFIRMING and result.receipt.source_tx_id == "at1delegated"
+ assert result.receipt.protocol_state == {"routeId": "hyperlane:aleo/wbtc->ethereum/wbtc", "sourceProgram": "hyp_warp_token_wbtc_v2.aleo",
+ "sourceFunction": "transfer_remote_as_signer", "amountAtomic": "10000",
+ "recipient": EVM1, "gasPaymentMicrocredits": "9138947"}
+ assert bridge.aleo.calls[-1] == ("hyp_warp_token_wbtc_v2.aleo", "transfer_remote_as_signer", call.inputs)
+
+
+def test_transfer_remote_pins_explicit_gas_payment(bridge):
+ call = bridge.hyperlane.transfer_remote("aleo/eth", EVM1, amount_atomic=1, gas_payment_microcredits=123)
+ assert call.function_name == "transfer_remote" and "amount: 123u64" in call.inputs[6]
+ assert "hyp_hook_manager.aleo" not in bridge.aleo.fetched # no quote read when pinned
+
+
+def test_is_delivered_reads_mailbox_deliveries(bridge):
+ assert bridge.hyperlane.is_delivered("0xc7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d") is True
+ assert bridge.hyperlane.is_delivered(bytes.fromhex("c7c2c763ef846ff1583d9222d8ecbfc56da2e0cdcc9a63bc4bde51467644794d")) is True
+ assert bridge.hyperlane.is_delivered("0x" + "00" * 32) is False
+ with pytest.raises(ConfigurationError, match="32-byte message id"):
+ bridge.hyperlane.is_delivered("0x1234")
diff --git a/bridge-sdk/tests/test_import_without_web3.py b/bridge-sdk/tests/test_import_without_web3.py
new file mode 100644
index 0000000..edd3550
--- /dev/null
+++ b/bridge-sdk/tests/test_import_without_web3.py
@@ -0,0 +1,28 @@
+"""``import aleo_bridge`` must succeed with web3/eth_account absent; only the first Ethereum(...)
+call that actually needs them raises MissingExtraError. Uses the same monkeypatch-and-revert
+pattern as test_package.py's test_import_without_optional_extras so the blocked modules and the
+reimported aleo_bridge never leak into tests that run after this one.
+"""
+from __future__ import annotations
+
+import importlib
+import sys
+
+import pytest
+
+
+def test_package_imports_without_web3(monkeypatch):
+ monkeypatch.setitem(sys.modules, "web3", None)
+ monkeypatch.setitem(sys.modules, "eth_account", None)
+ for name in list(sys.modules):
+ if name.startswith("aleo_bridge"):
+ monkeypatch.delitem(sys.modules, name)
+
+ pkg = importlib.import_module("aleo_bridge")
+ assert pkg.Ethereum is not None and pkg.EthModule is not None and pkg.EvmCall is not None
+
+ from aleo_bridge.errors import MissingExtraError
+
+ with pytest.raises(MissingExtraError) as exc_info:
+ pkg.Ethereum("http://127.0.0.1:1")
+ assert "aleo-bridge-sdk[evm]" in str(exc_info.value)
diff --git a/bridge-sdk/tests/test_keccak.py b/bridge-sdk/tests/test_keccak.py
new file mode 100644
index 0000000..0a16a1c
--- /dev/null
+++ b/bridge-sdk/tests/test_keccak.py
@@ -0,0 +1,22 @@
+import pytest
+
+from aleo_bridge._keccak import keccak256
+
+
+def test_keccak_known_answers():
+ assert keccak256(b"").hex() == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
+ assert keccak256(b"abc").hex() == "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
+ # Hyperlane Mailbox event topic — pinned by the whole ecosystem.
+ assert keccak256(b"DispatchId(bytes32)").hex() == \
+ "788dbc1b7152732178210e7f4d9d010ef016f9eafbe66786bd7169f56e0c353a"
+
+
+def test_keccak_accepts_bytearray_and_memoryview():
+ assert keccak256(bytearray(b"abc")) == keccak256(b"abc")
+ assert keccak256(memoryview(b"abc")) == keccak256(b"abc")
+
+
+def test_keccak_long_input_spans_blocks_and_agrees_with_web3():
+ web3 = pytest.importorskip("web3") # dev extra; the primitive still ships without it
+ for data in (bytes(range(256)) * 3, bytes(135), bytes(136), bytes(137), b"x" * 1000):
+ assert keccak256(data) == bytes(web3.Web3.keccak(data))
diff --git a/bridge-sdk/tests/test_live_helpers.py b/bridge-sdk/tests/test_live_helpers.py
new file mode 100644
index 0000000..f71d1b0
--- /dev/null
+++ b/bridge-sdk/tests/test_live_helpers.py
@@ -0,0 +1,979 @@
+"""Hermetic tests for the live-funds harness itself (port of veil `test/integration/live/helpers.test.ts`).
+
+Nothing here touches a network, a key, or a chain: the gates are exercised with a monkeypatched
+environment, the state files in ``tmp_path``, the Hyperlane explorer through an injected HTTP
+callable, and the rehearsal CLI against ``FakeBridge``. The harness is tested BEFORE the funded
+cases can run it, which is the whole point of the file: a bug in the gate is a bug that spends
+real money.
+"""
+from __future__ import annotations
+
+import json
+import os
+import stat
+
+import pytest
+
+from tests.live import config as live_config
+from tests.live import helpers as live_helpers
+
+FUNDS = "BRIDGE_LIVE_FUNDS"
+STATE_DIR = "BRIDGE_LIVE_STATE_DIR"
+ACK = "BRIDGE_LIVE_MAINNET_ACK"
+CASES = "BRIDGE_LIVE_MAINNET_CASES"
+EXECUTE = "BRIDGE_LIVE_MAINNET_EXECUTE"
+
+
+@pytest.fixture(autouse=True)
+def _clean_env(monkeypatch):
+ """Every gate variable starts unset: the truth tables below set exactly what they test."""
+ for name in (FUNDS, STATE_DIR, ACK, CASES, EXECUTE, "TEST_EVM_KEY"):
+ monkeypatch.delenv(name, raising=False)
+ return monkeypatch
+
+
+# ── config.py: reading required values without ever logging them ──────────────
+
+def test_required_names_the_variable_and_never_the_value(monkeypatch):
+ monkeypatch.setenv("TEST_EVM_KEY", " value-with-space ")
+ assert live_config.required("TEST_EVM_KEY") == "value-with-space"
+
+ with pytest.raises(live_config.LiveConfigError) as excinfo:
+ live_config.required("BRIDGE_LIVE_ABSENT")
+ assert "BRIDGE_LIVE_ABSENT" in str(excinfo.value)
+
+
+def test_required_treats_whitespace_only_as_missing(monkeypatch):
+ monkeypatch.setenv("TEST_EVM_KEY", " ")
+ with pytest.raises(live_config.LiveConfigError):
+ live_config.required("TEST_EVM_KEY")
+
+
+def test_normalizes_prefixed_and_unprefixed_evm_private_keys(monkeypatch):
+ """veil helpers.test.ts:14-20 — the 0x prefix is optional, the value is never echoed."""
+ key = "ab" * 32
+ monkeypatch.setenv("TEST_EVM_KEY", key)
+ assert live_config.required_evm_private_key("TEST_EVM_KEY") == f"0x{key}"
+ monkeypatch.setenv("TEST_EVM_KEY", f"0X{key}")
+ assert live_config.required_evm_private_key("TEST_EVM_KEY") == f"0x{key}"
+
+
+def test_rejects_a_malformed_evm_private_key_without_printing_it(monkeypatch):
+ monkeypatch.setenv("TEST_EVM_KEY", "deadbeef")
+ with pytest.raises(live_config.LiveConfigError) as excinfo:
+ live_config.required_evm_private_key("TEST_EVM_KEY")
+ message = str(excinfo.value)
+ assert "TEST_EVM_KEY" in message and "32" in message and "deadbeef" not in message
+
+
+# ── config.py: the gate truth table ───────────────────────────────────────────
+
+def test_live_funds_needs_both_the_flag_and_a_state_dir(monkeypatch):
+ assert live_config.live_funds_enabled() is False
+ monkeypatch.setenv(FUNDS, "1")
+ assert live_config.live_funds_enabled() is False # no state dir
+ monkeypatch.setenv(STATE_DIR, "/tmp/bridge-state")
+ assert live_config.live_funds_enabled() is True
+ monkeypatch.setenv(FUNDS, "true")
+ assert live_config.live_funds_enabled() is False # exactly "1", like veil
+
+
+def test_mainnet_case_requires_funding_state_acknowledgement_and_the_named_case(monkeypatch):
+ """veil helpers.test.ts:56-64."""
+ monkeypatch.setenv(FUNDS, "1")
+ monkeypatch.setenv(STATE_DIR, "/tmp/bridge-state")
+ monkeypatch.setenv(ACK, "I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS")
+ monkeypatch.setenv(CASES, "evm-xreserve, aleo-hyperlane")
+
+ assert live_config.mainnet_case_enabled("evm-xreserve") is True
+ assert live_config.mainnet_case_enabled("aleo-hyperlane") is True
+ assert live_config.mainnet_case_enabled("solana-hyperlane") is False
+
+ monkeypatch.setenv(ACK, "yes")
+ assert live_config.mainnet_case_enabled("evm-xreserve") is False
+ monkeypatch.setenv(ACK, "I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS")
+ monkeypatch.delenv(FUNDS)
+ assert live_config.mainnet_case_enabled("evm-xreserve") is False
+
+
+def test_execution_requires_a_separate_exact_acknowledgement(monkeypatch):
+ """veil helpers.test.ts:66-71 — nothing in this repo ever SETS this variable."""
+ assert live_config.mainnet_execution_enabled() is False
+ monkeypatch.setenv(EXECUTE, "yes")
+ assert live_config.mainnet_execution_enabled() is False
+ monkeypatch.setenv(EXECUTE, "I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS")
+ assert live_config.mainnet_execution_enabled() is True
+
+
+def test_gates_only_read_the_environment(monkeypatch):
+ """No gate may write, default or repair a variable — a gate that sets its own key is not a gate."""
+ before = dict(os.environ)
+ live_config.live_funds_enabled()
+ live_config.mainnet_case_enabled("evm-hyperlane")
+ live_config.mainnet_execution_enabled()
+ assert dict(os.environ) == before
+
+
+def test_case_names_are_veils_five_mainnet_cases():
+ assert live_config.CASE_NAMES == ("evm-hyperlane", "evm-xreserve", "aleo-hyperlane",
+ "aleo-xreserve", "solana-hyperlane")
+
+
+def test_one_atomic_unit_per_asset_precision():
+ """veil helpers.test.ts:73-77."""
+ assert live_config.one_atomic_unit(0) == "1"
+ assert live_config.one_atomic_unit(6) == "0.000001"
+ assert live_config.one_atomic_unit(9) == "0.000000001"
+ assert live_config.one_atomic_unit(18) == "0.000000000000000001"
+ for bad in (-1, 1.5, True):
+ with pytest.raises(live_config.LiveConfigError):
+ live_config.one_atomic_unit(bad)
+
+
+def test_live_state_path_is_namespaced_by_environment(monkeypatch, tmp_path):
+ monkeypatch.setenv(STATE_DIR, str(tmp_path))
+ assert live_config.live_state_path("mainnet", "evm-xreserve") == tmp_path / "mainnet" / "evm-xreserve.json"
+ with pytest.raises(live_config.LiveConfigError):
+ live_config.live_state_path("devnet", "evm-xreserve")
+ monkeypatch.delenv(STATE_DIR)
+ with pytest.raises(live_config.LiveConfigError):
+ live_config.live_state_path("mainnet", "evm-xreserve")
+
+
+def test_route_and_recipient_overrides_are_optional(monkeypatch):
+ assert live_config.case_route_override("evm-hyperlane") is None
+ monkeypatch.setenv("BRIDGE_LIVE_EVM_HYPERLANE_ROUTE_ID", "hyperlane:ethereum/wbtc->aleo/wbtc")
+ assert live_config.case_route_override("evm-hyperlane") == "hyperlane:ethereum/wbtc->aleo/wbtc"
+
+ assert live_config.recipient_override("aleo") is None
+ monkeypatch.setenv("BRIDGE_LIVE_ALEO_MAINNET_RECIPIENT", "aleo1recipient")
+ assert live_config.recipient_override("aleo") == "aleo1recipient"
+ with pytest.raises(live_config.LiveConfigError):
+ live_config.recipient_override("bitcoin")
+
+
+# ── helpers.py: state files ───────────────────────────────────────────────────
+
+def test_state_round_trips_and_starts_empty_for_an_absent_file(tmp_path):
+ """veil helpers.test.ts:34-41."""
+ path = tmp_path / "mainnet" / "state.json"
+ assert live_helpers.load_live_state(path, "route:a") == live_helpers.LiveState(route_id="route:a")
+
+ state = live_helpers.LiveState(route_id="route:a", source_tx_id="source-1")
+ live_helpers.save_live_state(path, state)
+ assert live_helpers.load_live_state(path, "route:a") == state
+
+
+def test_state_keeps_every_recorded_field_across_a_reload(tmp_path):
+ path = tmp_path / "state.json"
+ state = live_helpers.LiveState(
+ route_id="route:a", source_tx_id="0xsource", message_id="0xmessage",
+ destination_tx_id="at1destination", destination_balance_before="1000", completed=True,
+ checkpoint={"version": 1, "receiptId": "r"}, secret_nonce_present=True)
+ live_helpers.save_live_state(path, state)
+ assert live_helpers.load_live_state(path, "route:a") == state
+ assert json.loads(path.read_text())["routeId"] == "route:a" # veil's on-disk key names
+
+
+def test_state_files_are_owner_only_and_written_atomically(tmp_path):
+ path = tmp_path / "nested" / "state.json"
+ live_helpers.save_live_state(path, live_helpers.LiveState(route_id="route:a"))
+ assert stat.S_IMODE(path.stat().st_mode) == 0o600
+ assert stat.S_IMODE(path.parent.stat().st_mode) == 0o700
+ assert not list(path.parent.glob("*.tmp"))
+
+
+def test_state_fails_closed_for_corrupt_malformed_or_wrong_route_files(tmp_path):
+ """veil helpers.test.ts:43-52 — a state file we cannot trust never becomes a fresh run."""
+ path = tmp_path / "state.json"
+
+ path.write_text("{")
+ with pytest.raises(live_helpers.LiveStateError):
+ live_helpers.load_live_state(path, "route:a")
+
+ path.write_text(json.dumps([1, 2]))
+ with pytest.raises(live_helpers.LiveStateError):
+ live_helpers.load_live_state(path, "route:a")
+
+ path.write_text(json.dumps({"routeId": "route:b", "sourceTxId": "x"}))
+ with pytest.raises(live_helpers.LiveStateError, match="does not match"):
+ live_helpers.load_live_state(path, "route:a")
+
+ path.write_text(json.dumps({"routeId": "route:a", "sourceTxId": 1}))
+ with pytest.raises(live_helpers.LiveStateError, match="sourceTxId"):
+ live_helpers.load_live_state(path, "route:a")
+
+ path.write_text(json.dumps({"routeId": "route:a", "completed": "yes"}))
+ with pytest.raises(live_helpers.LiveStateError, match="completed"):
+ live_helpers.load_live_state(path, "route:a")
+
+ path.write_text(json.dumps({"routeId": "route:a", "checkpoint": "not-an-object"}))
+ with pytest.raises(live_helpers.LiveStateError, match="checkpoint"):
+ live_helpers.load_live_state(path, "route:a")
+
+
+# ── helpers.py: the secret nonce lives beside the state, never inside it ──────
+
+def test_secret_nonce_is_a_valid_scalar_and_freshly_random():
+ from aleo_bridge.encoding import validate_scalar
+
+ first = live_helpers.generate_secret_nonce()
+ assert validate_scalar(first) == first
+ assert first != live_helpers.generate_secret_nonce()
+
+
+def test_secret_file_is_exclusive_owner_only_and_absent_from_the_state(tmp_path):
+ path = tmp_path / "state.json"
+ nonce = live_helpers.ensure_secret_nonce(path)
+ secret = live_helpers.secret_path(path)
+
+ assert secret.name.endswith(".secret")
+ assert stat.S_IMODE(secret.stat().st_mode) == 0o600
+ assert live_helpers.load_secret_nonce(path) == nonce
+ assert live_helpers.ensure_secret_nonce(path) == nonce # stable across invocations
+
+ with pytest.raises(FileExistsError):
+ live_helpers.save_secret_nonce(path, nonce) # O_CREAT|O_EXCL: never overwritten
+
+ live_helpers.save_live_state(path, live_helpers.LiveState(route_id="route:a", secret_nonce_present=True))
+ assert nonce not in path.read_text()
+ assert json.loads(path.read_text())["secretNoncePresent"] is True
+
+
+def test_missing_secret_file_reads_as_none(tmp_path):
+ assert live_helpers.load_secret_nonce(tmp_path / "state.json") is None
+
+
+# ── helpers.py: benchmark marks ───────────────────────────────────────────────
+
+def test_benchmark_reports_per_step_and_total_elapsed_time():
+ """veil helpers.test.ts:22-32 (millisecond deltas from an injected clock)."""
+ times = iter([1.000, 1.250, 1.900])
+ lines: list[str] = []
+ benchmark = live_helpers.LiveBenchmark("route", now=lambda: next(times), log=lines.append)
+
+ benchmark.mark("quote-returned")
+ benchmark.mark("execute-returned")
+
+ assert lines == ["[route] quote-returned: +250ms (total 250ms)",
+ "[route] execute-returned: +650ms (total 900ms)"]
+ assert [m.step for m in benchmark.marks] == ["quote-returned", "execute-returned"]
+ assert [m.elapsed_ms for m in benchmark.marks] == [250, 650]
+ assert benchmark.summary() == "route: quote-returned +250ms, execute-returned +650ms (total 900ms)"
+
+
+def test_benchmark_summary_of_a_run_that_marked_nothing():
+ benchmark = live_helpers.LiveBenchmark("route", now=lambda: 0.0, log=lambda _: None)
+ assert benchmark.summary() == "route: no marks"
+
+
+# ── helpers.py: polling ───────────────────────────────────────────────────────
+
+def test_wait_for_returns_the_first_non_none_read_and_sleeps_between_polls():
+ reads = iter([None, None, "value"])
+ slept: list[float] = []
+ clock = iter([0.0, 15.0, 30.0, 45.0])
+
+ value = live_helpers.wait_for(lambda: next(reads), timeout_seconds=600, poll_seconds=15,
+ sleep=slept.append, now=lambda: next(clock))
+ assert value == "value" and slept == [15, 15]
+
+
+def test_wait_for_raises_at_the_deadline_and_names_the_state_file():
+ clock = iter([0.0, 5.0, 10.0, 10.0])
+ with pytest.raises(live_helpers.LiveTimeoutError, match="state file"):
+ live_helpers.wait_for(lambda: None, timeout_seconds=10, poll_seconds=1,
+ sleep=lambda _: None, now=lambda: next(clock))
+
+
+def test_wait_for_always_reads_at_least_once_even_with_a_zero_timeout():
+ calls = []
+
+ def read():
+ calls.append(1)
+ return "immediate"
+
+ assert live_helpers.wait_for(read, timeout_seconds=0, sleep=lambda _: None) == "immediate"
+ assert calls == [1]
+
+
+# ── helpers.py: the Hyperlane explorer (read-only HTTP) ───────────────────────
+
+class _Response:
+ def __init__(self, payload, status_code=200):
+ self._payload, self.status_code = payload, status_code
+
+ def json(self):
+ return self._payload
+
+
+DELIVERED = {"data": {"message_view": [{"msg_id": "\\xmessage", "is_delivered": True,
+ "destination_tx_hash": "\\xdestination"}]}}
+
+
+def test_hyperlane_lookup_uses_bytea_hashes_and_normalizes_the_result():
+ """veil helpers.test.ts:81-103 — the explorer speaks PostgreSQL bytea, the SDK speaks 0x."""
+ seen = {}
+
+ def post(url, payload, timeout):
+ seen.update(url=url, payload=payload)
+ return _Response(DELIVERED)
+
+ delivery = live_helpers.hyperlane_delivery("0xsource", post=post)
+ assert delivery == live_helpers.HyperlaneDelivery(message_id="0xmessage", destination_tx_id="0xdestination")
+ assert seen["url"] == live_helpers.HYPERLANE_EXPLORER_URL
+ assert "$hash: bytea!" in seen["payload"]["query"]
+ assert seen["payload"]["variables"]["hash"] == "\\xsource"
+
+
+def test_hyperlane_lookup_decodes_a_solana_base58_signature():
+ """veil helpers.test.ts:105-126."""
+ seen = {}
+
+ def post(url, payload, timeout):
+ seen.update(payload=payload)
+ return _Response(DELIVERED)
+
+ live_helpers.hyperlane_delivery(
+ "QrRfJM8xSiKgvqgd8PeiYTgyA7EkLbzKSnEn5wV6amxA4P15cQY41Vh4H85km8RvTX5pDph6oKxhVzsewdGhdnM", post=post)
+ assert seen["payload"]["variables"]["hash"] == (
+ "\\x1491b6d2018d56b09ce9e368e701ccfc618485ff784f6419fe72d660a4a992d5"
+ "f5d0a4392bf75b8172f57faeea28c3e660c0e9544e4320fb9f4df4d9cce9da06")
+
+
+def test_hyperlane_lookup_is_none_while_the_message_is_undelivered():
+ undelivered = {"data": {"message_view": [{"msg_id": "\\xmessage", "is_delivered": False}]}}
+ assert live_helpers.hyperlane_delivery("0xsource", post=lambda *a, **k: _Response(undelivered)) is None
+ assert live_helpers.hyperlane_delivery("0xsource", post=lambda *a, **k: _Response({"data": {"message_view": []}})) is None
+
+
+@pytest.mark.parametrize("status", [429, 500, 502, 503])
+def test_hyperlane_lookup_returns_none_when_the_explorer_rate_limits_or_fails(status):
+ """A throttled explorer is an environment condition, never a verdict on the transfer."""
+ assert live_helpers.hyperlane_delivery("0xsource", post=lambda *a, **k: _Response({}, status)) is None
+
+
+def test_hyperlane_lookup_surfaces_graphql_errors():
+ """veil helpers.test.ts:128-136 — a bad query must not poll silently until the timeout."""
+ errors = {"errors": [{"message": "invalid bytea input"}]}
+ with pytest.raises(live_helpers.ExplorerError, match="invalid bytea input"):
+ live_helpers.hyperlane_delivery("0xsource", post=lambda *a, **k: _Response(errors))
+
+
+def test_wait_for_hyperlane_delivery_gives_up_quietly_rather_than_failing_a_done_leg():
+ """The leg is already `done`; the destination-tx lookup is a convenience, not an assertion."""
+ assert live_helpers.wait_for_hyperlane_delivery(
+ "0xsource", post=lambda *a, **k: _Response({}, 429),
+ timeout_seconds=0, poll_seconds=0, sleep=lambda _: None) is None
+
+ delivery = live_helpers.wait_for_hyperlane_delivery(
+ "0xsource", post=lambda *a, **k: _Response(DELIVERED),
+ timeout_seconds=60, poll_seconds=0, sleep=lambda _: None)
+ assert delivery.destination_tx_id == "0xdestination"
+
+
+# ── helpers.py: Aleo confirmation ─────────────────────────────────────────────
+
+class _StatusBridge:
+ def __init__(self, statuses):
+ self.statuses = iter(statuses)
+ self.seen: list[str] = []
+
+ def status_of(self, tx_id):
+ self.seen.append(tx_id)
+ return next(self.statuses)
+
+
+def test_wait_for_aleo_transaction_accepts_after_pending(monkeypatch):
+ bridge = _StatusBridge([("pending", None), ("accepted", None)])
+ monkeypatch.setattr(live_helpers, "aleo_transaction_status", lambda b, tx: b.status_of(tx))
+ live_helpers.wait_for_aleo_transaction(bridge, "at1x", timeout_seconds=60, poll_seconds=0,
+ sleep=lambda _: None)
+ assert bridge.seen == ["at1x", "at1x"]
+
+
+def test_wait_for_aleo_transaction_raises_on_a_rejected_transaction(monkeypatch):
+ """veil helpers.test.ts:139-150 — a rejected transaction is a failure, not a slow confirmation."""
+ bridge = _StatusBridge([("rejected", "Aleo transaction at1rejected was rejected by the network")])
+ monkeypatch.setattr(live_helpers, "aleo_transaction_status", lambda b, tx: b.status_of(tx))
+ with pytest.raises(live_helpers.LiveCaseError, match="rejected"):
+ live_helpers.wait_for_aleo_transaction(bridge, "at1rejected", timeout_seconds=60, poll_seconds=0,
+ sleep=lambda _: None)
+
+
+def test_underfunded_carries_the_shortfall():
+ error = live_helpers.Underfunded(asset_id="ethereum/usdc", needed=2_000_000, have=1_500_000)
+ assert error.shortfall == 500_000
+ assert "ethereum/usdc" in str(error) and "500000" in str(error)
+
+
+# ══ cases.py ══════════════════════════════════════════════════════════════════
+
+from pathlib import Path # noqa: E402
+
+from aleo_bridge import lifecycle # noqa: E402
+from aleo_bridge.registry import DEFAULT_REGISTRY # noqa: E402
+from aleo_bridge.types import Progress, Receipt, Status # noqa: E402
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, FakeBridge # noqa: E402
+from tests.live import cases as live_cases # noqa: E402
+
+ETH_ROUTE = "hyperlane:ethereum/eth->aleo/eth"
+USDC_ROUTE = "xreserve:ethereum/usdc->aleo/usdcx"
+
+
+class LiveFakeBridge(FakeBridge):
+ """``FakeBridge`` plus the public lifecycle verbs — the surface ``cases.py`` is allowed to use.
+
+ The real ``Bridge`` methods are thin wrappers over ``lifecycle``; wiring the same functions onto
+ the fake keeps the harness honest (it may only call verbs that exist) without a second fake.
+ """
+
+ def quote(self, source, destination, **kwargs):
+ return lifecycle.quote(self, source=source, destination=destination, **kwargs)
+
+ def execute(self, plan, **kwargs):
+ return lifecycle.execute(self, plan, **kwargs)
+
+ def wait(self, progress, **kwargs):
+ return lifecycle.wait(self, progress, **kwargs)
+
+ def recover(self, checkpoint):
+ return lifecycle.recover(self, checkpoint)
+
+ def resume(self, progress, **kwargs):
+ return lifecycle.resume(self, progress, **kwargs)
+
+ def complete(self, progress, **kwargs):
+ return lifecycle.complete(self, progress, **kwargs)
+
+ def pending(self):
+ if self.checkpoints is None:
+ return []
+ return [lifecycle.progress_from_checkpoint(self.registry, cp) for cp in self.checkpoints.list()]
+
+
+@pytest.fixture
+def fake():
+ return LiveFakeBridge()
+
+
+def test_cases_are_veils_five_mainnet_cases_with_their_literals():
+ assert live_cases.CASE_NAMES == live_config.CASE_NAMES
+ assert live_cases.CASES["evm-xreserve"].amount == "2" # veil evm-xreserve:57
+ assert live_cases.CASES["evm-xreserve"].mint_mode == "private" # veil evm-xreserve:60
+ assert live_cases.CASES["aleo-xreserve"].amount == "2.000001" # veil aleo-xreserve:97
+ assert live_cases.CASES["aleo-xreserve"].mode == "private"
+ assert live_cases.CASES["aleo-hyperlane"].mode == "signer" # veil aleo-hyperlane:135
+ assert live_cases.CASES["evm-hyperlane"].amount is None # one atomic unit
+ assert all(spec.veil_source for spec in live_cases.CASES.values())
+
+
+def test_every_mainnet_route_is_covered_by_exactly_one_case():
+ """'All the routes back and forth': no mainnet route may be left without a case."""
+ routes = DEFAULT_REGISTRY.routes(environment="mainnet")
+ covered = {route.id: live_cases.case_for_route(DEFAULT_REGISTRY, route) for route in routes}
+ assert all(case is not None for case in covered.values()), \
+ [rid for rid, case in covered.items() if case is None]
+
+ by_case = {case: {route.id for route in live_cases.routes_for_case(DEFAULT_REGISTRY, case)}
+ for case in live_cases.CASE_NAMES}
+ assert set().union(*by_case.values()) == set(covered)
+ for left in live_cases.CASE_NAMES:
+ for right in live_cases.CASE_NAMES:
+ if left != right:
+ assert not by_case[left] & by_case[right]
+
+ active = {route.id for route in routes if route.active}
+ assert ETH_ROUTE in by_case["evm-hyperlane"] and "hyperlane:ethereum/wbtc->aleo/wbtc" in by_case["evm-hyperlane"]
+ assert "hyperlane:aleo/sol->solana/sol" in by_case["aleo-hyperlane"]
+ assert by_case["solana-hyperlane"] & active == {"hyperlane:solana/sol->aleo/sol"}
+ assert by_case["evm-xreserve"] & active == {USDC_ROUTE}
+ assert by_case["aleo-xreserve"] & active == {"xreserve:aleo/usdcx->ethereum/usdc"}
+
+
+def test_default_amount_is_one_atomic_unit_or_veils_literal():
+ route = DEFAULT_REGISTRY.route(ETH_ROUTE)
+ assert live_cases.default_amount(DEFAULT_REGISTRY, "evm-hyperlane", route) == "0.000000000000000001"
+ usdc = DEFAULT_REGISTRY.route(USDC_ROUTE)
+ assert live_cases.default_amount(DEFAULT_REGISTRY, "evm-xreserve", usdc) == "2"
+
+
+def test_state_names_are_route_qualified():
+ assert live_cases.state_name("evm-hyperlane", ETH_ROUTE) == "evm-hyperlane-hyperlane-ethereum-eth-aleo-eth"
+ assert live_cases.state_name("evm-hyperlane", "hyperlane:ethereum/wbtc->aleo/wbtc") \
+ != live_cases.state_name("evm-hyperlane", ETH_ROUTE)
+
+
+def test_quote_only_prints_the_table_and_submits_nothing(fake, tmp_path):
+ lines: list[str] = []
+ state_path = tmp_path / "evm-hyperlane.json"
+ state = live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=state_path,
+ execute=False, log=lines.append)
+
+ assert state.completed is False and state.checkpoint is None
+ assert not state_path.exists() # a rehearsal leaves no state behind
+ assert not any(event[0] in {"evm_send", "submit", "prove"} for event in fake.events)
+ printed = "\n".join(lines)
+ assert ETH_ROUTE in printed and "evm-hyperlane" in printed and "quote only" in printed
+
+
+def test_the_case_records_veils_benchmark_marks(fake, tmp_path):
+ benchmark = live_helpers.LiveBenchmark("evm-hyperlane", log=lambda _: None)
+ live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=tmp_path / "s.json",
+ execute=False, benchmark=benchmark, log=lambda _: None)
+ assert [mark.step for mark in benchmark.marks] == ["plan-prepared", "quote-returned"]
+
+
+def test_quote_only_never_creates_the_private_mint_secret(fake, tmp_path):
+ state_path = tmp_path / "evm-xreserve.json"
+ live_cases.run_case(fake, "evm-xreserve", USDC_ROUTE, state_path=state_path, execute=False,
+ log=lambda _: None)
+ assert not live_helpers.secret_path(state_path).exists()
+
+
+def test_underfunded_names_the_asset_and_the_shortfall(fake, tmp_path):
+ fake.eth.balances = {"ethereum/eth": 5}
+ with pytest.raises(live_helpers.Underfunded) as excinfo:
+ live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=tmp_path / "s.json",
+ execute=False, log=lambda _: None)
+ assert excinfo.value.asset_id == "ethereum/eth" and excinfo.value.shortfall > 0
+
+
+def test_a_completed_case_is_a_no_op_that_re_asserts_its_record(fake, tmp_path):
+ state_path = tmp_path / "done.json"
+ live_helpers.save_live_state(state_path, live_helpers.LiveState(
+ route_id=ETH_ROUTE, source_tx_id="0xsource", message_id="0xmessage",
+ destination_tx_id="at1destination", completed=True))
+
+ state = live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=state_path,
+ execute=True, log=lambda _: None)
+ assert state.completed and state.source_tx_id == "0xsource"
+ assert fake.calls == [] and fake.events == [] # nothing was quoted, nothing was submitted
+
+
+def test_a_completed_state_without_a_source_transaction_fails_closed(fake, tmp_path):
+ state_path = tmp_path / "bad.json"
+ live_helpers.save_live_state(state_path, live_helpers.LiveState(route_id=ETH_ROUTE, completed=True))
+ with pytest.raises(live_helpers.LiveCaseError):
+ live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=state_path, execute=True,
+ log=lambda _: None)
+
+
+def test_an_inactive_route_is_refused_before_anything_is_read(fake, tmp_path):
+ with pytest.raises(live_helpers.LiveCaseError, match="metadata-required"):
+ live_cases.run_case(fake, "evm-hyperlane", "hyperlane:ethereum/usad->aleo/usad",
+ state_path=tmp_path / "s.json", execute=False, log=lambda _: None)
+
+
+def test_recipient_and_sender_default_to_our_own_addresses(fake):
+ route = DEFAULT_REGISTRY.route(ETH_ROUTE)
+ assert live_cases.default_recipient(fake, route) == ALEO_RECIPIENT
+ assert live_cases.sender_for(fake, route) == EVM_ADDRESS
+
+ outbound = DEFAULT_REGISTRY.route("hyperlane:aleo/eth->ethereum/eth")
+ assert live_cases.default_recipient(fake, outbound) == EVM_ADDRESS
+ assert live_cases.sender_for(fake, outbound) == ALEO_RECIPIENT
+
+
+def test_an_unconfigured_destination_chain_is_reported_not_an_attribute_error():
+ """Ruling: probe ``bridge.solana``/``bridge.ethereum``; ``bridge.sol``/``bridge.eth`` RAISE."""
+ aleo_only = LiveFakeBridge(ethereum=False, solana=False)
+ assert aleo_only.ethereum is None and aleo_only.solana is None
+ route = DEFAULT_REGISTRY.route("hyperlane:aleo/sol->solana/sol")
+ with pytest.raises(live_helpers.LiveCaseError, match="solana"):
+ live_cases.default_recipient(aleo_only, route)
+ aleo_only.public_balances = {"aleo/sol": 7}
+ assert live_cases.read_balances(aleo_only) == {"aleo/sol": 7} # the Aleo row still reads
+
+
+def _xreserve_quote(**extra):
+ from aleo_bridge.types import EvmXReserveQuote
+
+ plan = lifecycle.prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="2", recipient=ALEO_RECIPIENT, mint_mode="private")
+ return EvmXReserveQuote(kind="evm-xreserve", plan=plan, fees=(), amount_out="2", hook_data=b"",
+ remote_recipient_bytes32=b"\x00" * 32, balance_atomic=5_000_000,
+ allowance_atomic=0, approval_required=True, **extra)
+
+
+def test_print_quote_renders_the_xreserve_max_fee_as_the_protocol_fee_line(fake):
+ """``EvmXReserveQuote.fees`` is empty: its max fee is the protocol cost a human has to see."""
+ lines: list[str] = []
+ live_cases.print_quote(fake, _xreserve_quote(max_fee_atomic=100_000),
+ case="evm-xreserve", route_id=USDC_ROUTE, log=lines.append)
+ printed = "\n".join(lines)
+ assert "0.1 USDC [xReserve max fee]" in printed
+ assert "max_fee_atomic" in printed and ALEO_RECIPIENT in printed
+ assert "scalar" not in printed
+
+
+# ── the drive loop: wait → resume → complete → done, and never execute twice ──
+
+class _ScriptedBridge:
+ """A bridge whose lifecycle verbs return a scripted sequence, to test ``_drive`` in isolation."""
+
+ def __init__(self, steps):
+ self.steps, self.calls = list(steps), []
+ self.plan = lifecycle.prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="2", recipient=ALEO_RECIPIENT, mint_mode="private")
+
+ def _next(self, verb):
+ self.calls.append(verb)
+ state = self.steps.pop(0)
+ receipt = Receipt(id="r1", protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="0xsource",
+ protocol_state={"routeId": USDC_ROUTE, "messageId": "0xmessage"})
+ return Progress(state, self.plan, receipt, error="scripted failure" if state == "failed" else None)
+
+ def wait(self, progress, **kwargs):
+ return self._next("wait")
+
+ def resume(self, progress, **kwargs):
+ return self._next("resume")
+
+ def complete(self, progress, **kwargs):
+ return self._next("complete")
+
+ def execute(self, *args, **kwargs):
+ raise AssertionError("execute must never be called from the drive loop")
+
+
+def _drive(bridge, first, **kwargs):
+ benchmark = live_helpers.LiveBenchmark("t", log=lambda _: None)
+ progress = Progress(first, bridge.plan,
+ Receipt(id="r1", protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ protocol_state={"routeId": USDC_ROUTE}))
+ return live_cases._drive(bridge, progress, live_helpers.LiveState(route_id=USDC_ROUTE),
+ Path("/nonexistent/state.json"), spec=live_cases.CASES["evm-xreserve"],
+ benchmark=benchmark, save=lambda _: None, wait_timeout_seconds=1,
+ wait_poll_seconds=0, log=lambda _: None, **kwargs)
+
+
+def test_drive_runs_wait_resume_complete_until_done():
+ bridge = _ScriptedBridge(["resume", "wait", "complete", "wait", "done"])
+ progress = _drive(bridge, "wait", secret_nonce="7scalar")
+ assert progress.next == "done"
+ assert bridge.calls == ["wait", "resume", "wait", "complete", "wait"]
+
+
+def test_drive_raises_the_reported_error_on_failure():
+ bridge = _ScriptedBridge(["failed"])
+ with pytest.raises(live_helpers.LiveCaseError, match="scripted failure"):
+ _drive(bridge, "wait", secret_nonce="7scalar")
+
+
+def test_drive_refuses_a_private_mint_without_the_kept_nonce():
+ bridge = _ScriptedBridge(["complete"])
+ with pytest.raises(live_helpers.LiveCaseError, match="secret nonce"):
+ _drive(bridge, "wait", secret_nonce=None)
+
+
+def test_drive_gives_up_rather_than_looping_forever():
+ bridge = _ScriptedBridge(["wait"] * 40)
+ with pytest.raises(live_helpers.LiveCaseError, match="did not settle"):
+ _drive(bridge, "wait", secret_nonce="7scalar")
+
+
+# ══ scripts/rehearse.py ═══════════════════════════════════════════════════════
+
+import importlib.util # noqa: E402
+
+from aleo_bridge.errors import PollingTimeoutError # noqa: E402
+
+SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "rehearse.py"
+
+
+@pytest.fixture(scope="module")
+def rehearse():
+ spec = importlib.util.spec_from_file_location("rehearse", SCRIPT)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+@pytest.fixture
+def state_dir(monkeypatch, tmp_path):
+ monkeypatch.setenv(STATE_DIR, str(tmp_path))
+ return tmp_path
+
+
+def _report(path):
+ return json.loads(Path(path).read_text())
+
+
+def test_cli_parses_the_documented_flags(rehearse):
+ args = rehearse.parse_args(["--case", "evm-hyperlane"])
+ assert args.case == "evm-hyperlane" and args.route is None
+ assert args.quote_only is False and args.recover is None and args.report is None
+
+ args = rehearse.parse_args(["--case", "aleo-xreserve", "--route", "xreserve:aleo/usdcx->ethereum/usdc",
+ "--quote-only", "--report", "/tmp/r.json"])
+ assert args.route == "xreserve:aleo/usdcx->ethereum/usdc" and args.quote_only and args.report == "/tmp/r.json"
+
+ args = rehearse.parse_args(["--recover", "/tmp/state.json"])
+ assert args.recover == "/tmp/state.json" and args.case is None
+
+ for bad in ([], ["--case", "not-a-case"]):
+ with pytest.raises(SystemExit):
+ rehearse.parse_args(bad)
+
+
+def test_cli_has_no_reset_flag(rehearse):
+ """veil's solana-deposit.ts --reset deletes a checkpoint without checking the chain; not ported."""
+ with pytest.raises(SystemExit):
+ rehearse.parse_args(["--case", "evm-hyperlane", "--reset"])
+
+
+def test_quote_only_run_reports_every_route_of_the_case(rehearse, fake, state_dir, tmp_path):
+ lines: list[str] = []
+ report = tmp_path / "report.json"
+ code = rehearse.run(["--case", "evm-hyperlane", "--quote-only", "--report", str(report)],
+ bridge_factory=lambda: fake, log=lines.append)
+
+ assert code == rehearse.EXIT_OK
+ payload = _report(report)
+ rows = {row["route_id"]: row for row in payload["results"]}
+ assert set(rows) == {route.id for route in live_cases.routes_for_case(DEFAULT_REGISTRY, "evm-hyperlane")}
+ assert rows[ETH_ROUTE]["status"] == "quote-only"
+ assert rows["hyperlane:ethereum/usad->aleo/usad"]["status"] == "skipped"
+ assert "metadata-required" in rows["hyperlane:ethereum/usad->aleo/usad"]["reason"]
+ assert payload["execute"] is False and payload["case"] == "evm-hyperlane"
+
+ printed = "\n".join(lines)
+ assert ETH_ROUTE in printed and "quote-only" in printed
+ assert not any(event[0] in {"evm_send", "submit"} for event in fake.events)
+
+
+def test_a_single_route_can_be_selected(rehearse, fake, state_dir, tmp_path):
+ report = tmp_path / "report.json"
+ rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--quote-only", "--report", str(report)],
+ bridge_factory=lambda: fake, log=lambda _: None)
+ assert [row["route_id"] for row in _report(report)["results"]] == [ETH_ROUTE]
+
+
+def test_without_the_acknowledgements_the_run_quotes_and_names_the_variable(rehearse, fake, state_dir,
+ monkeypatch, tmp_path):
+ """The gate is READ here; nothing prints a line that would set it in a subshell."""
+ monkeypatch.setenv(FUNDS, "1")
+ monkeypatch.setenv(ACK, "I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS")
+ monkeypatch.setenv(CASES, "evm-hyperlane") # the case is acknowledged, submission is not
+ lines: list[str] = []
+ report = tmp_path / "report.json"
+ code = rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--report", str(report)],
+ bridge_factory=lambda: fake, log=lines.append)
+
+ payload = _report(report)
+ assert code == rehearse.EXIT_OK and payload["execute"] is False
+ assert "BRIDGE_LIVE_MAINNET_EXECUTE" in payload["reason"]
+ printed = "\n".join(lines)
+ assert "export " not in printed and "I_ACKNOWLEDGE" not in printed
+ assert "BRIDGE_LIVE_MAINNET_EXECUTE" in printed
+
+
+def test_acknowledged_runs_ask_the_case_to_execute(rehearse, fake, state_dir, monkeypatch, tmp_path):
+ monkeypatch.setenv(FUNDS, "1")
+ monkeypatch.setenv(ACK, "I_ACKNOWLEDGE_BRIDGE_MAINNET_FUNDS")
+ monkeypatch.setenv(CASES, "evm-hyperlane")
+ monkeypatch.setenv(EXECUTE, "I_ACKNOWLEDGE_THIS_SUBMITS_MAINNET_TRANSACTIONS")
+ seen = {}
+
+ def runner(bridge, route_id, **kwargs):
+ seen.update(route_id=route_id, execute=kwargs["execute"], state_path=kwargs["state_path"])
+ return live_helpers.LiveState(route_id=route_id, source_tx_id="0xsource", message_id="0xm",
+ destination_tx_id="at1d", completed=True)
+
+ monkeypatch.setitem(live_cases.RUNNERS, "evm-hyperlane", runner)
+ report = tmp_path / "report.json"
+ code = rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--report", str(report)],
+ bridge_factory=lambda: fake, log=lambda _: None)
+
+ assert code == rehearse.EXIT_OK and seen["execute"] is True
+ assert Path(seen["state_path"]) == state_dir / "mainnet" / f"{live_cases.state_name('evm-hyperlane', ETH_ROUTE)}.json"
+ row = _report(report)["results"][0]
+ assert row["status"] == "completed" and row["source_tx_id"] == "0xsource" and row["message_id"] == "0xm"
+
+
+def test_an_underfunded_case_is_skipped_with_its_shortfall(rehearse, fake, state_dir, monkeypatch, tmp_path):
+ def runner(bridge, route_id, **kwargs):
+ raise live_helpers.Underfunded(asset_id="ethereum/eth", needed=1000, have=1)
+
+ monkeypatch.setitem(live_cases.RUNNERS, "evm-hyperlane", runner)
+ report = tmp_path / "report.json"
+ code = rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--quote-only", "--report", str(report)],
+ bridge_factory=lambda: fake, log=lambda _: None)
+ row = _report(report)["results"][0]
+ assert code == rehearse.EXIT_OK and row["status"] == "skipped" and "999" in row["reason"]
+
+
+def test_a_timeout_is_pending_with_the_resume_command_not_a_failure(rehearse, fake, state_dir, monkeypatch, tmp_path):
+ def runner(bridge, route_id, **kwargs):
+ raise PollingTimeoutError("still in flight", status=Status.DELIVERY_PENDING)
+
+ monkeypatch.setitem(live_cases.RUNNERS, "evm-hyperlane", runner)
+ report = tmp_path / "report.json"
+ lines: list[str] = []
+ code = rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--quote-only", "--report", str(report)],
+ bridge_factory=lambda: fake, log=lines.append)
+ row = _report(report)["results"][0]
+ assert code == rehearse.EXIT_PENDING and row["status"] == "pending"
+ assert "--recover" in row["resume"] and row["resume"] in "\n".join(lines)
+
+
+def test_a_failed_case_exits_one(rehearse, fake, state_dir, monkeypatch, tmp_path):
+ def runner(bridge, route_id, **kwargs):
+ raise live_helpers.LiveCaseError("the destination rejected it")
+
+ monkeypatch.setitem(live_cases.RUNNERS, "evm-hyperlane", runner)
+ report = tmp_path / "report.json"
+ code = rehearse.run(["--case", "evm-hyperlane", "--route", ETH_ROUTE, "--quote-only", "--report", str(report)],
+ bridge_factory=lambda: fake, log=lambda _: None)
+ row = _report(report)["results"][0]
+ assert code == rehearse.EXIT_FAILED and row["status"] == "failed" and "rejected" in row["reason"]
+
+
+def test_recover_resolves_the_case_from_the_state_file(rehearse, fake, state_dir, monkeypatch, tmp_path):
+ state_path = state_dir / "mainnet" / "evm-hyperlane-resume.json"
+ live_helpers.save_live_state(state_path, live_helpers.LiveState(route_id=ETH_ROUTE, source_tx_id="0xs"))
+ seen = {}
+
+ def runner(bridge, route_id, **kwargs):
+ seen.update(route_id=route_id, state_path=kwargs["state_path"])
+ return live_helpers.LiveState(route_id=route_id, source_tx_id="0xs", completed=True)
+
+ monkeypatch.setitem(live_cases.RUNNERS, "evm-hyperlane", runner)
+ code = rehearse.run(["--recover", str(state_path), "--quote-only"],
+ bridge_factory=lambda: fake, log=lambda _: None)
+ assert code == rehearse.EXIT_OK
+ assert seen["route_id"] == ETH_ROUTE and Path(seen["state_path"]) == state_path
+
+
+def test_the_table_renders_one_line_per_route(rehearse):
+ rows = [{"case": "evm-hyperlane", "route_id": ETH_ROUTE, "status": "quote-only", "reason": "",
+ "source_tx_id": None, "message_id": None, "destination_tx_id": None, "resume": ""},
+ {"case": "evm-hyperlane", "route_id": "hyperlane:ethereum/wbtc->aleo/wbtc", "status": "skipped",
+ "reason": "registry availability: metadata-required", "source_tx_id": None,
+ "message_id": None, "destination_tx_id": None, "resume": ""}]
+ table = rehearse.render_table(rows)
+ assert ETH_ROUTE in table and "quote-only" in table and "metadata-required" in table
+ assert len(table.strip().splitlines()) >= 3 # header + two rows
+
+
+# ══ 13b: environment aliases, the execute handover, the suite's parametrization ══
+
+def test_key_and_rpc_variables_resolve_per_environment(monkeypatch):
+ """A testnet run may never reach for the mainnet Aleo key, and an unset RPC has a default."""
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", "APrivateKey1zkpMainnet")
+ monkeypatch.setenv("ALEO_E2E_PRIVATE_KEY", "APrivateKey1zkpTestnet")
+ assert live_config.aleo_private_key("mainnet") == "APrivateKey1zkpMainnet"
+ assert live_config.aleo_private_key("testnet") == "APrivateKey1zkpTestnet"
+
+ monkeypatch.setenv("BRIDGE_LIVE_ALEO_TESTNET_PRIVATE_KEY", "APrivateKey1zkpAlias")
+ assert live_config.aleo_private_key("testnet") == "APrivateKey1zkpAlias" # the alias wins
+ assert live_config.aleo_private_key("mainnet") == "APrivateKey1zkpMainnet"
+
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY")
+ with pytest.raises(live_config.LiveConfigError, match="BRIDGE_PRIVATE_KEY"):
+ live_config.aleo_private_key("mainnet")
+ with pytest.raises(live_config.LiveConfigError, match="environment"):
+ live_config.aleo_private_key("devnet")
+
+
+def test_evm_key_is_normalised_and_the_value_never_appears(monkeypatch):
+ monkeypatch.setenv("BRIDGE_EVM_PRIVATE_KEY", "0x" + "AB" * 32)
+ assert live_config.evm_private_key("mainnet") == "0x" + "ab" * 32
+ assert live_config.evm_private_key("testnet") == "0x" + "ab" * 32
+
+ monkeypatch.setenv("BRIDGE_LIVE_EVM_TESTNET_PRIVATE_KEY", "0x" + "cd" * 32)
+ assert live_config.evm_private_key("testnet") == "0x" + "cd" * 32
+ assert live_config.evm_private_key("mainnet") == "0x" + "ab" * 32
+
+ monkeypatch.setenv("BRIDGE_LIVE_EVM_TESTNET_PRIVATE_KEY", "not-a-key")
+ with pytest.raises(live_config.LiveConfigError) as excinfo:
+ live_config.evm_private_key("testnet")
+ assert "not-a-key" not in str(excinfo.value)
+
+
+def test_rpc_urls_fall_back_to_the_public_defaults(monkeypatch):
+ for name in ("SEPOLIA_RPC_URL", "BRIDGE_LIVE_SEPOLIA_RPC_URL", "ETHEREUM_RPC_URL",
+ "BRIDGE_LIVE_ETHEREUM_RPC_URL", "ALEO_ENDPOINT", "BRIDGE_LIVE_ALEO_ENDPOINT"):
+ monkeypatch.delenv(name, raising=False)
+ assert live_config.evm_rpc_url("mainnet") == live_config.DEFAULT_ETHEREUM_RPC_URL
+ assert live_config.evm_rpc_url("testnet") == live_config.DEFAULT_SEPOLIA_RPC_URL
+ assert live_config.aleo_endpoint() == live_config.DEFAULT_ALEO_ENDPOINT
+
+ monkeypatch.setenv("SEPOLIA_RPC_URL", "https://sepolia.example")
+ monkeypatch.setenv("BRIDGE_LIVE_ETHEREUM_RPC_URL", "https://eth.example")
+ monkeypatch.setenv("ALEO_ENDPOINT", "https://aleo.example/api")
+ assert live_config.evm_rpc_url("testnet") == "https://sepolia.example"
+ assert live_config.evm_rpc_url("mainnet") == "https://eth.example"
+ assert live_config.aleo_endpoint() == "https://aleo.example/api"
+ assert live_config.first_value(("ABSENT_A", "SEPOLIA_RPC_URL")) == ("SEPOLIA_RPC_URL", "https://sepolia.example")
+ assert live_config.first_value(("ABSENT_A", "ABSENT_B")) is None
+
+
+def test_amount_overrides_are_per_case_with_an_xreserve_shorthand(monkeypatch):
+ monkeypatch.delenv("BRIDGE_LIVE_XRESERVE_AMOUNT", raising=False)
+ assert live_config.case_amount_override("evm-xreserve") is None
+ monkeypatch.setenv("BRIDGE_LIVE_XRESERVE_AMOUNT", "3")
+ assert live_config.case_amount_override("evm-xreserve") == "3"
+ assert live_config.case_amount_override("aleo-xreserve") == "3"
+ assert live_config.case_amount_override("evm-hyperlane") is None # never a Hyperlane amount
+ monkeypatch.setenv("BRIDGE_LIVE_EVM_XRESERVE_AMOUNT", "5")
+ assert live_config.case_amount_override("evm-xreserve") == "5"
+
+
+def _refuse_execute(*args, **kwargs):
+ raise AssertionError("execute must never be called for a transfer that already has a checkpoint")
+
+
+def test_stop_after_execute_hands_the_transfer_over_through_the_state_file(fake, tmp_path):
+ """Phase one executes and returns; phase two must reach done without executing again."""
+ state_path = tmp_path / "handover.json"
+ first = live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=state_path, execute=True,
+ stop_after_execute=True, wait_timeout_seconds=1, wait_poll_seconds=0,
+ log=lambda _: None)
+ assert first.checkpoint is not None and first.completed is False and first.source_tx_id
+ assert state_path.exists()
+
+ submitted = [event for event in fake.events if event[0] in {"evm_send", "submit"}]
+ fake.execute = _refuse_execute # the handover may only use the recovery verbs
+ fake.eth.recover_result = Receipt(
+ id=first.checkpoint["receiptId"], protocol="hyperlane", status=Status.COMPLETED,
+ source_tx_id=first.source_tx_id, destination_tx_id="at1delivered",
+ protocol_state={"routeId": ETH_ROUTE, "messageId": "0x" + "ee" * 32})
+ second = live_cases.run_case(fake, "evm-hyperlane", ETH_ROUTE, state_path=state_path, execute=True,
+ wait_timeout_seconds=1, wait_poll_seconds=0, log=lambda _: None)
+ assert second.completed and second.source_tx_id == first.source_tx_id
+ assert [event for event in fake.events if event[0] in {"evm_send", "submit"}] == submitted
+
+
+def test_the_suite_parametrizes_every_route_in_both_environments():
+ from tests.live import test_lifecycle_live as suite
+
+ for environment, buckets in (("mainnet", suite.MAINNET_ROUTES), ("testnet", suite.TESTNET_ROUTES)):
+ listed = {route.id for routes in buckets.values() for route in routes}
+ assert listed == {route.id for route in DEFAULT_REGISTRY.routes(environment=environment)}
+
+ mainnet = {route.id: route for routes in suite.MAINNET_ROUTES.values() for route in routes}
+ assert "hyperlane:ethereum/usad->aleo/usad" in mainnet # metadata-required, parametrized
+ assert not mainnet["hyperlane:ethereum/usad->aleo/usad"].active
+ assert suite.TESTNET_DEPOSIT_ROUTE in {r.id for r in suite.TESTNET_ROUTES["evm-xreserve"]}
+ assert suite.TESTNET_RETURN_ROUTE in {r.id for r in suite.TESTNET_ROUTES["aleo-xreserve"]}
+ assert suite.TESTNET_DEPOSIT_AMOUNT == "3" and suite.TESTNET_RETURN_AMOUNT == "2.000001"
+ assert "--recover" in suite.resume_command("/tmp/state.json")
+
+
+def test_only_the_aleo_to_evm_withdrawal_measures_delivery_by_balance():
+ """The one leg with no delivery query anywhere: `wait` on it could only ever time out."""
+ rise = {route.id for route in DEFAULT_REGISTRY.routes()
+ if live_cases.delivery_is_a_balance_rise(route, DEFAULT_REGISTRY)}
+ assert rise == {"xreserve:aleo/usdcx->ethereum/usdc", "xreserve:aleo-testnet/usdcx->sepolia/usdc"}
+ assert not live_cases.delivery_is_a_balance_rise(DEFAULT_REGISTRY.route(USDC_ROUTE), DEFAULT_REGISTRY)
+ assert not live_cases.delivery_is_a_balance_rise(DEFAULT_REGISTRY.route(ETH_ROUTE), DEFAULT_REGISTRY)
+
+
+def test_the_suite_sets_no_acknowledgement_and_prints_no_settable_form():
+ source = (SCRIPT.parent.parent / "tests" / "live" / "test_lifecycle_live.py").read_text(encoding="utf-8")
+ assert "export " not in source
+ assert live_config.MAINNET_ACK not in source and live_config.MAINNET_EXECUTE_ACK not in source
+ assert "setenv" not in source and "os.environ[" not in source
diff --git a/bridge-sdk/tests/test_mcp.py b/bridge-sdk/tests/test_mcp.py
new file mode 100644
index 0000000..0b52fcf
--- /dev/null
+++ b/bridge-sdk/tests/test_mcp.py
@@ -0,0 +1,84 @@
+"""MCP stdio server over the agent tools — schema fidelity, dispatch, and the confirm gate.
+
+Every test here exercises the module's functions directly (``tool_definitions`` / ``call_tool`` /
+the handlers ``build_server`` registers) against a :class:`FakeBridge`. None of them start a real
+stdio server: that would require a live client on the other end of the pipe, which is exactly what
+the low-level ``mcp.server.Server`` API lets us skip in tests.
+"""
+import json
+import sys
+
+import pytest
+
+mcp = pytest.importorskip("mcp")
+
+from mcp import types # noqa: E402
+
+from aleo_bridge.agent import bridge_tools # noqa: E402
+from aleo_bridge.errors import MissingExtraError # noqa: E402
+from aleo_bridge.mcp import build_server, call_tool, tool_definitions # noqa: E402
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, FakeBridge # noqa: E402
+
+
+def test_tool_definitions_carry_exact_schemas():
+ defs = {t.name: t for t in tool_definitions()}
+ expected = {t["name"]: t for t in bridge_tools()}
+ assert set(defs) == set(expected)
+ assert defs["bridge_execute"].inputSchema == expected["bridge_execute"]["input_schema"]
+ assert "confirm" in defs["bridge_execute"].inputSchema["properties"]
+ for name, tool in defs.items():
+ assert tool.description == expected[name]["description"]
+
+
+def test_build_server_constructs():
+ assert build_server(FakeBridge()).name == "aleo-bridge"
+
+
+async def test_call_tool_dispatches_and_serializes():
+ b = FakeBridge()
+ out = await call_tool(b, "bridge_quote", {"source": "ethereum/usdc", "destination": "aleo/usdcx",
+ "amount": "2", "recipient": ALEO_RECIPIENT})
+ assert out[0].type == "text"
+ assert json.loads(out[0].text)["kind"] == "evm-xreserve"
+ gated = await call_tool(b, "bridge_execute", {"source": "ethereum/usdc", "destination": "aleo/usdcx",
+ "amount": "2", "recipient": ALEO_RECIPIENT})
+ assert json.loads(gated[0].text)["confirmation_required"] is True and b.events == []
+
+
+async def test_build_server_handlers_list_and_dispatch_through_the_real_server_wiring():
+ """Reach the exact coroutines ``@server.list_tools()``/``@server.call_tool()`` registered —
+ not just the module-level helpers they delegate to — and confirm the write gate still holds
+ when a tool call is routed through the server object."""
+ b = FakeBridge()
+ server = build_server(b)
+
+ list_result = await server.request_handlers[types.ListToolsRequest](types.ListToolsRequest())
+ names = {t.name for t in list_result.root.tools}
+ assert names == {t["name"] for t in bridge_tools()}
+
+ call_result = await server.request_handlers[types.CallToolRequest](
+ types.CallToolRequest(params=types.CallToolRequestParams(
+ name="bridge_execute",
+ arguments={"source": "ethereum/usdc", "destination": "aleo/usdcx", "amount": "2",
+ "recipient": ALEO_RECIPIENT})))
+ payload = json.loads(call_result.root.content[0].text)
+ assert payload["confirmation_required"] is True
+ assert b.events == []
+
+
+def test_missing_extra_error_without_mcp_installed(monkeypatch):
+ """``tool_definitions``/``build_server`` import ``mcp`` lazily: without the extra installed,
+ calling them fails with the SDK's own ``MissingExtraError`` (naming the real ``mcp`` extra from
+ ``pyproject.toml``), never a bare ``ImportError`` from deep inside the module."""
+ for mod in ("mcp", "mcp.types", "mcp.server", "mcp.server.stdio"):
+ monkeypatch.setitem(sys.modules, mod, None)
+
+ from aleo_bridge import mcp as bridge_mcp
+
+ with pytest.raises(MissingExtraError) as exc_info:
+ bridge_mcp.tool_definitions()
+ assert exc_info.value.extra == "mcp"
+ assert "aleo-bridge-sdk[mcp]" in str(exc_info.value)
+
+ with pytest.raises(MissingExtraError):
+ bridge_mcp.build_server(FakeBridge())
diff --git a/bridge-sdk/tests/test_package.py b/bridge-sdk/tests/test_package.py
new file mode 100644
index 0000000..9fc7157
--- /dev/null
+++ b/bridge-sdk/tests/test_package.py
@@ -0,0 +1,85 @@
+import importlib
+import re
+import sys
+import tomllib
+from pathlib import Path
+
+import pytest
+
+import aleo_bridge
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def test_version_is_pinned_in_lockstep():
+ pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
+ assert pyproject["project"]["version"] == "0.1.0" == aleo_bridge.__version__
+ assert pyproject["project"]["name"] == "aleo-bridge-sdk"
+ extras = pyproject["project"]["optional-dependencies"]
+ assert {"evm", "solana", "mcp", "dev"} <= set(extras)
+ assert any(dep.startswith("mcp>=1") and "<2" in dep for dep in extras["mcp"])
+
+
+def test_wheel_ships_agents_md():
+ assert (ROOT / "python" / "aleo_bridge" / "AGENTS.md").exists()
+ assert aleo_bridge.agent_guide().startswith("# aleo-bridge")
+
+
+def test_readme_covers_the_journey():
+ readme = (ROOT / "README.md").read_text()
+ for needle in ("Bridge.from_env()", "Bridge.from_profile()", "Ethereum(", "Solana(", "progress.next",
+ "recover", "resume", "complete", "shield", "unshield", "python -m aleo_bridge.mcp",
+ "scripts/rehearse.py", "BRIDGE_PRIVATE_KEY", "EVM_PRIVATE_KEY", "SOLANA_PRIVATE_KEY",
+ "BRIDGE_LIVE_MAINNET_EXECUTE", "secret_nonce", "| `resume` |", "| `complete` |"):
+ assert needle in readme, needle
+ assert "Co-Authored-By" not in readme
+ # every active mainnet route appears in the route table
+ for route_id in ("xreserve:ethereum/usdc->aleo/usdcx", "hyperlane:ethereum/eth->aleo/eth",
+ "hyperlane:aleo/sol->solana/sol"):
+ assert route_id in readme
+
+
+def test_ci_has_bridge_jobs():
+ workflow = (ROOT.parent / ".github" / "workflows" / "sdk-wheels.yml").read_text()
+ assert workflow.count("'bridge-sdk/**'") == 2
+ for job in ("build-bridge:", "release-bridge:"):
+ assert job in workflow
+ assert "environment: pypi-bridge" in workflow
+ assert 'pip install "$(ls bridge-sdk/dist/*.whl)[evm,solana,mcp]"' in workflow
+ assert re.search(r'pip install "\$\(ls bridge-sdk/dist/\*\.whl\)"\s*\n\s*python -c "import aleo_bridge', workflow)
+
+
+def test_import_without_optional_extras(monkeypatch):
+ for mod in ("web3", "eth_account", "solders", "solana", "mcp"):
+ monkeypatch.setitem(sys.modules, mod, None) # any import of these now raises ImportError
+ for name in list(sys.modules):
+ if name.startswith("aleo_bridge"):
+ 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.1.0"
+ assert issubclass(pkg.RouteNotFoundError, pkg.BridgeError)
+
+
+def test_error_hierarchy_and_messages():
+ from aleo_bridge import errors as e
+
+ for cls in (e.ConfigurationError, e.MissingExtraError, e.RouteNotFoundError, e.AmbiguousRouteError,
+ e.RouteUnavailableError, e.RegistryVersionMismatchError, e.UnsupportedRouteError,
+ e.InvalidAmountError, e.InvalidRecipientError, e.InsufficientBalanceError,
+ e.ChainMismatchError, e.NotResumableError, e.CheckpointInvalidError, e.AttestationError,
+ e.DeliveryUnknownError, e.PollingTimeoutError):
+ assert issubclass(cls, e.BridgeError)
+ err = e.MissingExtraError("evm", "Ethereum connections")
+ assert err.extra == "evm"
+ assert "pip install 'aleo-bridge-sdk[evm]'" in str(err)
+
+
+def test_polling_timeout_carries_status():
+ from aleo_bridge.errors import PollingTimeoutError
+
+ err = PollingTimeoutError("Bridge status polling timed out in state DELIVERY_PENDING",
+ status="DELIVERY_PENDING", progress=None)
+ assert err.status == "DELIVERY_PENDING" and err.progress is None
+ with pytest.raises(PollingTimeoutError):
+ raise err
diff --git a/bridge-sdk/tests/test_prepare.py b/bridge-sdk/tests/test_prepare.py
new file mode 100644
index 0000000..9888f1b
--- /dev/null
+++ b/bridge-sdk/tests/test_prepare.py
@@ -0,0 +1,201 @@
+import pytest
+
+from aleo_bridge._plan import build_plan
+from aleo_bridge.errors import (AmbiguousRouteError, ConfigurationError, InvalidAmountError,
+ InvalidRecipientError, RouteNotFoundError,
+ RegistryVersionMismatchError, CheckpointInvalidError)
+from aleo_bridge.lifecycle import prepare, resolve_route
+from aleo_bridge.registry import DEFAULT_REGISTRY
+from aleo_bridge.types import Plan
+
+ALEO = "aleo1" + "a" * 58
+EVM1 = "0x0000000000000000000000000000000000000001"
+SOLANA1 = "11111111111111111111111111111111"
+
+
+def test_resolves_route_from_asset_refs():
+ plan = prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO)
+ assert plan.route_id == "xreserve:ethereum/usdc->aleo/usdcx"
+ assert plan.protocol == "xreserve" and plan.environment == "mainnet"
+ assert plan.registry_version == DEFAULT_REGISTRY.version
+ assert (plan.amount, plan.amount_atomic) == ("25", 25_000_000)
+ assert plan.mint_mode == "public" and plan.sender is None
+ # tuple refs and case-insensitive keys work too
+ assert prepare(DEFAULT_REGISTRY, source=("ethereum", "USDC"), destination=("aleo", "usdcx"),
+ amount="25", recipient=ALEO).route_id == plan.route_id
+
+
+def test_protocol_filter_is_forwarded_and_disambiguates():
+ class _Ambiguous:
+ version = DEFAULT_REGISTRY.version
+ asset = DEFAULT_REGISTRY.asset
+ chain = DEFAULT_REGISTRY.chain
+ seen = []
+
+ def find_route(self, source, destination, protocol=None):
+ self.seen.append(protocol)
+ if protocol is None:
+ raise AmbiguousRouteError("Multiple bridge routes match; specify protocol")
+ return DEFAULT_REGISTRY.find_route(source, destination, protocol)
+
+ reg = _Ambiguous()
+ with pytest.raises(AmbiguousRouteError):
+ prepare(reg, source="ethereum/usdc", destination="aleo/usdcx", amount="25", recipient=ALEO)
+ plan = prepare(reg, source="ethereum/usdc", destination="aleo/usdcx", amount="25",
+ recipient=ALEO, protocol="xreserve")
+ assert plan.route_id == "xreserve:ethereum/usdc->aleo/usdcx" and reg.seen == [None, "xreserve"]
+ with pytest.raises(RouteNotFoundError):
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO, protocol="hyperlane")
+
+
+def test_xreserve_deposit_steps():
+ plan = prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25.5", recipient=ALEO)
+ assert [s.id for s in plan.steps] == ["source-approval", "source-deposit",
+ "deposit-attestation", "destination-mint"]
+ assert [s.kind for s in plan.steps] == ["approve", "deposit", "wait-attestation", "mint"]
+ assert [s.id for s in plan.steps if s.irreversible] == ["source-deposit"]
+ assert [s.executor for s in plan.steps] == ["evm-wallet", "evm-wallet", "protocol", "protocol"]
+
+
+def test_xreserve_burn_steps():
+ plan = prepare(DEFAULT_REGISTRY, source="aleo/usdcx", destination="ethereum/usdc",
+ amount="10", recipient=EVM1)
+ assert [s.id for s in plan.steps] == ["source-burn", "withdrawal-attestation",
+ "destination-withdrawal", "destination-confirmation"]
+ assert [s.kind for s in plan.steps] == ["burn", "wait-attestation", "withdraw", "confirm-delivery"]
+ assert [s.executor for s in plan.steps] == ["aleo-wallet", "protocol", "protocol", "protocol"]
+ assert [s.id for s in plan.steps if s.irreversible] == ["source-burn"]
+
+
+def test_mint_modes():
+ record = prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO, mint_mode="record")
+ private = prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO, mint_mode="private")
+ assert record.mint_mode == "record" and record.steps[-1].executor == "protocol"
+ assert private.mint_mode == "private" and private.steps[-1].executor == "aleo-wallet"
+ with pytest.raises(ConfigurationError, match="only valid.*Aleo"):
+ prepare(DEFAULT_REGISTRY, source="aleo/usdcx", destination="ethereum/usdc",
+ amount="10", recipient=EVM1, mint_mode="private")
+ with pytest.raises(ConfigurationError, match="xReserve"):
+ prepare(DEFAULT_REGISTRY, source="ethereum/wbtc", destination="aleo/wbtc",
+ amount="0.1", recipient=ALEO, mint_mode="record")
+ with pytest.raises(ConfigurationError, match="mint_mode"):
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO, mint_mode="secret")
+
+
+def test_hyperlane_steps_approval_only_on_non_aleo_token_sources():
+ inbound = prepare(DEFAULT_REGISTRY, source="ethereum/wbtc", destination="aleo/wbtc",
+ amount="0.1", recipient=ALEO)
+ assert [s.id for s in inbound.steps] == ["source-approval", "source-dispatch",
+ "message-delivery", "destination-confirmation"]
+ assert [s.kind for s in inbound.steps] == ["approve", "dispatch", "wait-delivery", "confirm-delivery"]
+ assert inbound.steps[0].executor == "evm-wallet" and inbound.steps[-1].executor == "protocol"
+ assert [s.id for s in inbound.steps if s.irreversible] == ["source-dispatch"]
+
+ native = prepare(DEFAULT_REGISTRY, source="ethereum/eth", destination="aleo/eth",
+ amount="0.000000000000000001", recipient=ALEO)
+ assert [s.id for s in native.steps] == ["source-dispatch", "message-delivery", "destination-confirmation"]
+ assert native.amount_atomic == 1
+
+ outbound = prepare(DEFAULT_REGISTRY, source="aleo/wbtc", destination="ethereum/wbtc",
+ amount="0.1", recipient=EVM1)
+ assert [s.id for s in outbound.steps] == ["source-dispatch", "message-delivery", "destination-confirmation"]
+ assert outbound.steps[0].executor == "aleo-wallet"
+
+ sol = prepare(DEFAULT_REGISTRY, source="solana/sol", destination="aleo/sol",
+ amount="0.000000001", recipient=ALEO, sender="11111111111111111111111111111111")
+ assert [s.id for s in sol.steps] == ["source-dispatch", "message-delivery", "destination-confirmation"]
+ assert sol.steps[0].executor == "solana-wallet" and sol.sender == "11111111111111111111111111111111"
+
+
+def test_amount_forms_and_precision():
+ by_atomic = prepare(DEFAULT_REGISTRY, source="ethereum/wbtc", destination="aleo/wbtc",
+ amount_atomic=100_000, recipient=ALEO)
+ assert (by_atomic.amount, by_atomic.amount_atomic) == ("0.001", 100_000)
+ with pytest.raises(InvalidAmountError, match="greater than zero"):
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="0", recipient=ALEO)
+ with pytest.raises(InvalidAmountError): # 7 fractional digits on a 6-decimal asset
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="0.0000001", recipient=ALEO)
+ with pytest.raises(InvalidAmountError): # exactly one of amount / amount_atomic
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="1", amount_atomic=1_000_000, recipient=ALEO)
+ with pytest.raises(InvalidAmountError):
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx", recipient=ALEO)
+
+
+def test_destination_decimals_are_checked_too():
+ class _Reg:
+ version = DEFAULT_REGISTRY.version
+ chain = DEFAULT_REGISTRY.chain
+ find_route = DEFAULT_REGISTRY.find_route
+
+ def asset(self, ref):
+ asset = DEFAULT_REGISTRY.asset(ref)
+ if asset.id == "aleo/usdcx":
+ import dataclasses
+ return dataclasses.replace(asset, decimals=2) # coarser destination
+ return asset
+
+ with pytest.raises(InvalidAmountError):
+ prepare(_Reg(), source="ethereum/usdc", destination="aleo/usdcx", amount="1.001", recipient=ALEO)
+ assert prepare(_Reg(), source="ethereum/usdc", destination="aleo/usdcx", amount="1.5",
+ recipient=ALEO).amount_atomic == 1_500_000
+
+
+def test_recipient_regex():
+ with pytest.raises(InvalidRecipientError, match="aleo address format"):
+ prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="1", recipient="not-an-aleo-address")
+ with pytest.raises(InvalidRecipientError, match="ethereum address format"):
+ prepare(DEFAULT_REGISTRY, source="aleo/wbtc", destination="ethereum/wbtc",
+ amount="0.1", recipient=ALEO)
+
+
+def test_metadata_required_routes_still_plan():
+ # veil's prepare only excludes *disabled* routes; quote/execute refuse metadata-required ones.
+ plan = prepare(DEFAULT_REGISTRY, source="aleo/aleo", destination="ethereum/aleo",
+ amount="1", recipient=EVM1)
+ assert plan.route_id == "hyperlane:aleo/aleo->ethereum/aleo"
+
+
+def test_plan_roundtrip_and_resolve_route():
+ plan = prepare(DEFAULT_REGISTRY, source="ethereum/usdc", destination="aleo/usdcx",
+ amount="25", recipient=ALEO, mint_mode="private", sender=EVM1)
+ again = Plan.from_dict(plan.to_dict())
+ assert again == plan
+ resolved = resolve_route(DEFAULT_REGISTRY, plan)
+ assert (resolved.route.id, resolved.source_chain.family, resolved.destination_chain.family) == (
+ plan.route_id, "evm", "aleo")
+ import dataclasses
+ with pytest.raises(RegistryVersionMismatchError):
+ resolve_route(DEFAULT_REGISTRY, dataclasses.replace(plan, registry_version="old"))
+ with pytest.raises(CheckpointInvalidError):
+ resolve_route(DEFAULT_REGISTRY, dataclasses.replace(plan, protocol="hyperlane"))
+
+
+def _recipient_for(chain_family: str) -> str:
+ return {"aleo": ALEO, "evm": EVM1, "solana": SOLANA1}[chain_family]
+
+
+def test_prepare_equals_build_plan_for_every_active_route():
+ # Controller ruling (task-1-controller-notes.md #4): prepare() is only ever a thin
+ # validating wrapper around the shared _plan.build_plan — for every active route this
+ # must hold field-by-field, with amount_atomic=1 and a recipient valid for the destination.
+ for route in DEFAULT_REGISTRY.routes(include_unavailable=True):
+ if not route.active:
+ continue
+ destination = DEFAULT_REGISTRY.asset(route.destination_asset_id)
+ destination_chain = DEFAULT_REGISTRY.chain(destination.chain_id)
+ recipient = _recipient_for(destination_chain.family)
+ prepared = prepare(DEFAULT_REGISTRY, source=route.source_asset_id,
+ destination=route.destination_asset_id, amount_atomic=1,
+ recipient=recipient, protocol=route.protocol)
+ expected = build_plan(DEFAULT_REGISTRY, route, amount_atomic=1, recipient=recipient, sender=None)
+ assert prepared == expected, route.id
diff --git a/bridge-sdk/tests/test_privacy.py b/bridge-sdk/tests/test_privacy.py
new file mode 100644
index 0000000..4314133
--- /dev/null
+++ b/bridge-sdk/tests/test_privacy.py
@@ -0,0 +1,101 @@
+import pytest
+
+from aleo_bridge.errors import ConfigurationError, InsufficientBalanceError, InvalidAmountError, InvalidRecipientError, UnsupportedRouteError
+from aleo_bridge.freezelist import EMPTY_MERKLE_PROOF_PAIR, EMPTY_TREE_ROOT
+from aleo_bridge.privacy import record_amount
+from aleo_bridge.types import PrivacyReceipt
+from tests.conftest import SIGNER, USDCX_RECORD, USDCX_RECORD_SMALL
+
+OTHER = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+SOL_RECORD = f"{{ owner: {SIGNER}.private, amount: 300000000u128.private, _nonce: 9group.public }}"
+SOL_RECORD_BIG = f"{{ owner: {SIGNER}.private, amount: 900000000u128.private, _nonce: 10group.public }}"
+
+
+def test_record_amount_parser():
+ assert record_amount(USDCX_RECORD) == 5_000_000 and record_amount(USDCX_RECORD_SMALL) == 100
+ assert record_amount("{ owner: aleo1x.private, _nonce: 1group.public }") is None
+ assert record_amount("") is None
+
+
+def test_shield_arc20_matches_veil(bridge):
+ call = bridge.privacy.shield("aleo/eth", amount="0.000000000000000001")
+ assert (call.program_id, call.function_name, call.inputs) == ("arc20_eth.aleo", "shield", ["1u128"])
+ result = call.transact()
+ assert result == PrivacyReceipt("at1built", "aleo/eth", "0.000000000000000001", 1, "shield")
+
+
+def test_shield_arc22_names_recipient(bridge):
+ call = bridge.privacy.shield(("aleo", "usdcx"), amount="2.5")
+ assert (call.program_id, call.function_name, call.inputs) == ("usdcx_stablecoin.aleo", "transfer_public_to_private", [SIGNER, "2500000u128"])
+ assert bridge.privacy.shield("aleo/usdcx", amount_atomic=2_500_000, recipient=OTHER).inputs == [OTHER, "2500000u128"]
+ with pytest.raises(ConfigurationError, match="ARC-20 shield always credits the caller"):
+ bridge.privacy.shield("aleo/eth", amount="1", recipient=OTHER)
+ with pytest.raises(InvalidRecipientError):
+ bridge.privacy.shield("aleo/usdcx", amount="1", recipient="0x1234")
+
+
+def test_unshield_arc20_selects_smallest_covering_record(bridge):
+ bridge.aleo.record_rows = [{"program": "arc20_sol.aleo", "record_plaintext": SOL_RECORD_BIG},
+ {"program": "arc20_sol.aleo", "record_plaintext": SOL_RECORD},
+ {"program": "usdcx_stablecoin.aleo", "record_plaintext": USDCX_RECORD}]
+ call = bridge.privacy.unshield("aleo/sol", amount="0.25")
+ assert (call.program_id, call.function_name, call.inputs) == ("arc20_sol.aleo", "unshield", [SOL_RECORD, "250000000u128"])
+ assert bridge.aleo.record_queries[-1] == {"program": "arc20_sol.aleo", "record": "Token", "unspent": True}
+ result = call.delegate(wait=False)
+ assert result == PrivacyReceipt("at1delegated", "aleo/sol", "0.25", 250_000_000, "unshield")
+ with pytest.raises(ConfigurationError, match="ARC-20 unshield takes no Merkle proof"):
+ bridge.privacy.unshield("aleo/sol", amount="0.25", merkle_proof="[x]")
+
+
+def test_unshield_arc20_recipient_must_match_caller(bridge):
+ bridge.aleo.record_rows = [{"program": "arc20_sol.aleo", "record_plaintext": SOL_RECORD}]
+ call = bridge.privacy.unshield("aleo/sol", amount="0.25", recipient=SIGNER)
+ assert (call.program_id, call.function_name, call.inputs) == ("arc20_sol.aleo", "unshield", [SOL_RECORD, "250000000u128"])
+ with pytest.raises(ConfigurationError, match="ARC-20 unshield always credits the caller"):
+ bridge.privacy.unshield("aleo/sol", amount="0.25", recipient=OTHER)
+
+
+def test_unshield_arc22_defaults_to_signer_record_and_empty_proof(bridge):
+ bridge.aleo.mappings.setdefault("usdcx_freezelist.aleo", {})["freeze_list_root"] = {"1u8": f"{EMPTY_TREE_ROOT}field"}
+ call = bridge.privacy.unshield("aleo/usdcx", amount="2.5")
+ assert (call.program_id, call.function_name) == ("usdcx_stablecoin.aleo", "transfer_private_to_public")
+ assert call.inputs[:3] == [SIGNER, "2500000u128", USDCX_RECORD]
+ assert call.inputs[3] == EMPTY_MERKLE_PROOF_PAIR and call.inputs[3].count("0field") == 32
+
+
+def test_unshield_arc22_accepts_explicit_inputs(bridge):
+ call = bridge.privacy.unshield("aleo/usdcx", amount="2.5", recipient=OTHER, record=USDCX_RECORD_SMALL, merkle_proof="[custom-proof]")
+ assert call.inputs == [OTHER, "2500000u128", USDCX_RECORD_SMALL, "[custom-proof]"]
+ assert bridge.aleo.record_queries == [] # explicit record: no scanner query
+
+
+def test_unsupported_assets_and_zero_amounts(bridge):
+ with pytest.raises(UnsupportedRouteError, match="does not support shielding"):
+ bridge.privacy.shield("aleo/aleo", amount="1")
+ with pytest.raises(UnsupportedRouteError, match="does not support unshielding"):
+ bridge.privacy.unshield("aleo/aleo", amount="1")
+ with pytest.raises(UnsupportedRouteError, match="does not support shielding"):
+ bridge.privacy.shield("ethereum/usdc", amount="1")
+ with pytest.raises(InvalidAmountError, match="Unshielding amount must be greater than zero"):
+ bridge.privacy.unshield("aleo/sol", amount="0")
+ with pytest.raises(InvalidAmountError, match="Shielding amount must be greater than zero"):
+ bridge.privacy.shield("aleo/sol", amount_atomic=0)
+
+
+def test_select_record_reports_largest_available(bridge):
+ bridge.aleo.record_rows = [{"program": "usdcx_stablecoin.aleo", "record_plaintext": USDCX_RECORD_SMALL}]
+ with pytest.raises(InsufficientBalanceError, match="largest available is 100"):
+ bridge.privacy.select_record("usdcx_stablecoin.aleo", 2_500_000)
+ bridge.aleo.record_rows = []
+ with pytest.raises(InsufficientBalanceError, match="largest available is 0"):
+ bridge.privacy.select_record("usdcx_stablecoin.aleo", 1)
+
+
+def test_private_burn_defaults_resolve_through_privacy_and_freezelist(bridge):
+ ONE_LIT = "[" + ",".join(["0u8"] * 31 + ["1u8"]) + "]"
+ # The freeze-list root must be readable on chain for the default (unsupplied) merkle_proof=
+ # to resolve — an unreadable root is now fatal (see test_freezelist.py).
+ bridge.aleo.mappings.setdefault("usdcx_freezelist.aleo", {})["freeze_list_root"] = {"1u8": f"{EMPTY_TREE_ROOT}field"}
+ call = bridge.xreserve.burn("0x0000000000000000000000000000000000000001", amount="2.5")
+ assert call.inputs == [USDCX_RECORD, "2500000u128", "0u32", ONE_LIT, EMPTY_MERKLE_PROOF_PAIR]
+ assert bridge.aleo.record_queries[-1]["program"] == "usdcx_stablecoin.aleo"
diff --git a/bridge-sdk/tests/test_profile.py b/bridge-sdk/tests/test_profile.py
new file mode 100644
index 0000000..30d61ea
--- /dev/null
+++ b/bridge-sdk/tests/test_profile.py
@@ -0,0 +1,76 @@
+import json
+import os
+import stat
+
+import pytest
+
+from aleo_bridge.errors import ConfigurationError
+from aleo_bridge.profile import DEFAULT_ENDPOINT, Profile
+
+
+def test_profile_created_once_with_private_mode(tmp_path, monkeypatch):
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY", raising=False)
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY_FILE", raising=False)
+ profile = Profile.load_or_create(tmp_path / "home")
+ assert profile.address.startswith("aleo1") and len(profile.address) == 63
+ assert profile.private_key.startswith("APrivateKey1") and profile.network == "mainnet" and profile.endpoint == DEFAULT_ENDPOINT
+ assert stat.S_IMODE(os.stat(tmp_path / "home" / "profile.json").st_mode) == 0o600
+ again = Profile.load_or_create(tmp_path / "home", network="testnet", endpoint="https://other.example")
+ assert (again.address, again.network, again.endpoint) == (profile.address, "mainnet", DEFAULT_ENDPOINT) # creation-only args
+ data = json.loads((tmp_path / "home" / "profile.json").read_text())
+ assert set(data) == {"address", "private_key", "network", "endpoint"}
+ assert profile.checkpoint_dir == tmp_path / "home" / "checkpoints" and profile.checkpoint_dir.is_dir()
+
+
+def test_profile_imports_key_from_env_or_file(tmp_path, monkeypatch):
+ from aleo import testnet as net
+ key = net.PrivateKey.random()
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", str(key))
+ assert Profile.load_or_create(tmp_path / "a", network="testnet").address == str(key.address)
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY")
+ key_file = tmp_path / "key.txt"
+ key_file.write_text(f"{key}\n")
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY_FILE", str(key_file))
+ assert Profile.load_or_create(tmp_path / "b", network="testnet").address == str(key.address)
+
+
+def test_default_home_and_tilde_expansion(tmp_path, monkeypatch):
+ monkeypatch.setenv("ALEO_BRIDGE_HOME", str(tmp_path / "x"))
+ assert Profile.default_home() == tmp_path / "x"
+ monkeypatch.delenv("ALEO_BRIDGE_HOME")
+ assert Profile.default_home().name == ".aleo-bridge"
+ monkeypatch.setenv("HOME", str(tmp_path))
+ assert Profile.load_or_create("~/p", network="testnet").home == tmp_path / "p"
+
+
+def test_profile_rejects_unknown_network(tmp_path):
+ with pytest.raises(ConfigurationError, match="network"):
+ Profile.load_or_create(tmp_path / "bad", network="devnet")
+
+
+def test_profile_creation_is_exclusive_loser_adopts_winner_key(tmp_path, monkeypatch):
+ """Two processes racing Profile.load_or_create() on a fresh home must not clobber each other:
+ the exclusive create (O_EXCL) means the loser adopts the winner's already-written key."""
+ from aleo import mainnet as net
+
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY", raising=False)
+ monkeypatch.delenv("BRIDGE_PRIVATE_KEY_FILE", raising=False)
+ home = tmp_path / "home"
+ winner_key = net.PrivateKey.random()
+ winner_address = str(winner_key.address)
+
+ def racing_initial_key(network):
+ # Simulate another process winning the race: it creates the home dir and profile.json
+ # before this process gets to its own exclusive-open attempt.
+ home.mkdir(parents=True, exist_ok=True, mode=0o700)
+ (home / "profile.json").write_text(json.dumps({
+ "address": winner_address, "private_key": str(winner_key), "network": network, "endpoint": DEFAULT_ENDPOINT,
+ }))
+ loser_key = net.PrivateKey.random() # must NOT end up written or returned
+ return str(loser_key), str(loser_key.address)
+
+ monkeypatch.setattr("aleo_bridge.profile._initial_key", racing_initial_key)
+ profile = Profile.load_or_create(home)
+ assert profile.address == winner_address
+ assert json.loads((home / "profile.json").read_text())["address"] == winner_address
+ assert stat.S_IMODE(os.stat(home).st_mode) == 0o700
diff --git a/bridge-sdk/tests/test_quote.py b/bridge-sdk/tests/test_quote.py
new file mode 100644
index 0000000..1da052e
--- /dev/null
+++ b/bridge-sdk/tests/test_quote.py
@@ -0,0 +1,64 @@
+import pytest
+
+from aleo_bridge.errors import (ConfigurationError, InvalidAmountError, RouteUnavailableError,
+ UnsupportedRouteError)
+from aleo_bridge.lifecycle import quote
+from aleo_bridge.types import AleoHyperlaneQuote, AleoXReserveQuote
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+
+
+def test_evm_hyperlane_quote_dispatches_to_eth_and_carries_the_canonical_plan():
+ b = FakeBridge()
+ q = quote(b, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001", recipient=ALEO_RECIPIENT,
+ sender=EVM_ADDRESS)
+ assert q.kind == "evm-hyperlane" and q.plan.route_id == "hyperlane:ethereum/wbtc->aleo/wbtc"
+ assert q.plan.sender == EVM_ADDRESS and q.native_value_atomic == 100_000 + 1000
+ assert b.calls == [("eth.quote_transfer_remote", {"plan": q.plan})]
+
+
+def test_evm_xreserve_quote_passes_mint_mode_and_secret_nonce():
+ b = FakeBridge()
+ q = quote(b, source="ethereum/usdc", destination="aleo/usdcx", amount="2", recipient=ALEO_RECIPIENT,
+ mint_mode="private", secret_nonce="7scalar")
+ assert q.kind == "evm-xreserve" and q.plan.mint_mode == "private"
+ assert b.calls[-1] == ("eth.quote_deposit_usdc", {"plan": q.plan, "secret_nonce": "7scalar"})
+
+
+def test_solana_hyperlane_quote():
+ b = FakeBridge(solana=True)
+ q = quote(b, source="solana/sol", destination="aleo/sol", amount="0.000000001", recipient=ALEO_RECIPIENT,
+ sender=SOL_ADDRESS)
+ assert q.kind == "solana-hyperlane" and q.total_lamports == 1 + 7_914_240
+ assert b.calls == [("sol.quote_transfer_remote", {"plan": q.plan})]
+
+
+def test_aleo_hyperlane_quote_reads_the_igp_only():
+ b = FakeBridge(ethereum=False)
+ q = quote(b, source="aleo/eth", destination="ethereum/eth", amount="0.000000000000000001",
+ recipient=EVM_ADDRESS)
+ assert isinstance(q, AleoHyperlaneQuote) and q.kind == "aleo-hyperlane"
+ assert q.payment_microcredits == 8_174_147 and q.gas_limit == 44_000
+ assert q.amount_out == "0.000000000000000001"
+ assert [f.kind for f in q.fees] == ["protocol"] and q.fees[0].amount == "8.174147" and q.fees[0].estimated
+ assert q.fees[0].asset_id == "aleo/aleo"
+ assert b.calls == [("hyperlane.quote_gas_payment", "aleo/eth")]
+
+
+def test_aleo_xreserve_quote_is_offline_and_deducts_the_withdrawal_fee():
+ b = FakeBridge(ethereum=False)
+ q = quote(b, source="aleo/usdcx", destination="ethereum/usdc", amount="2.000001", recipient=EVM_ADDRESS)
+ assert isinstance(q, AleoXReserveQuote) and q.kind == "aleo-xreserve"
+ assert q.amount_out == "0.000001" and q.withdrawal_fee_atomic == 2_000_000
+ assert q.fees == (q.fees[0],) and q.fees[0].kind == "protocol" and q.fees[0].estimated is False
+ assert (q.fees[0].chain_id, q.fees[0].asset_id, q.fees[0].amount) == ("aleo", "aleo/usdcx", "2")
+ assert b.calls == [] # no network
+ with pytest.raises(InvalidAmountError, match="exceed the 2 USDCx withdrawal fee"):
+ quote(b, source="aleo/usdcx", destination="ethereum/usdc", amount="2", recipient=EVM_ADDRESS)
+
+
+def test_missing_connection_and_unavailable_route():
+ b = FakeBridge(ethereum=False)
+ with pytest.raises(ConfigurationError, match="Ethereum connection"):
+ quote(b, source="ethereum/usdc", destination="aleo/usdcx", amount="2", recipient=ALEO_RECIPIENT)
+ with pytest.raises(RouteUnavailableError, match="metadata-required"):
+ quote(b, source="aleo/aleo", destination="ethereum/aleo", amount="1", recipient=EVM_ADDRESS)
diff --git a/bridge-sdk/tests/test_recover.py b/bridge-sdk/tests/test_recover.py
new file mode 100644
index 0000000..44cbdfc
--- /dev/null
+++ b/bridge-sdk/tests/test_recover.py
@@ -0,0 +1,230 @@
+import json
+
+import pytest
+
+from aleo_bridge.checkpoint import Checkpoint, FileCheckpointStore, create_checkpoint
+from aleo_bridge.errors import (CheckpointInvalidError, RegistryVersionMismatchError, UnsupportedRouteError)
+from aleo_bridge.lifecycle import prepare, recover
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+
+EVM1 = EVM_ADDRESS
+
+
+def _aleo_eth_checkpoint(b, **source):
+ plan = prepare(b.registry, source="aleo/eth", destination="ethereum/eth", amount="0.000000000000000001",
+ recipient=EVM1)
+ return plan, {"version": 1,
+ "intent": {"source": {"chain": "aleo", "asset": "eth"}, "destination": {"chain": "ethereum", "asset": "eth"},
+ "bridgeProtocol": "hyperlane", "amount": plan.amount, "recipient": plan.recipient},
+ "route": {"id": plan.route_id, "registryVersion": plan.registry_version},
+ "source": source}
+
+
+def test_version_route_and_registry_checks():
+ b = FakeBridge(ethereum=False)
+ plan, cp = _aleo_eth_checkpoint(b, transactionId="at1x")
+ with pytest.raises(CheckpointInvalidError, match="version"):
+ recover(b, {**cp, "version": 2})
+ with pytest.raises(CheckpointInvalidError, match="does not match"):
+ recover(b, {**cp, "route": {**cp["route"], "id": "hyperlane:aleo/wbtc->ethereum/wbtc"}})
+ with pytest.raises(RegistryVersionMismatchError):
+ recover(b, {**cp, "route": {**cp["route"], "registryVersion": "2020-01-01.old"}})
+ with pytest.raises(CheckpointInvalidError, match="intent"):
+ recover(b, {**cp, "intent": {"source": {"chain": "aleo"}}})
+
+
+def test_prepared_but_unbroadcast_aleo_transaction_resumes_without_network():
+ b = FakeBridge(ethereum=False)
+ serialized = json.dumps({"type": "execute", "id": "at1prepared", "fee": {}})
+ plan, cp = _aleo_eth_checkpoint(b, preparedTransaction={"transactionId": "at1prepared",
+ "serializedTransaction": serialized})
+ cp["deliveryVerification"] = {"balanceBeforeAtomic": "100", "expectedIncreaseAtomic": "1"}
+ progress = recover(b, json.dumps(cp)) # JSON string accepted
+ assert progress.next == "resume" and progress.receipt.status is Status.SOURCE_SUBMISSION_PENDING
+ assert progress.receipt.id == "at1prepared"
+ assert progress.receipt.protocol_state == {"routeId": plan.route_id, "preparedTransaction": serialized,
+ "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"}
+ assert b.calls == [] and b.events == []
+ with pytest.raises(CheckpointInvalidError, match="id does not match"):
+ recover(b, {**cp, "source": {"preparedTransaction": {"transactionId": "at1other", "serializedTransaction": serialized}}})
+ with pytest.raises(CheckpointInvalidError, match="invalid prepared"):
+ recover(b, {**cp, "source": {"preparedTransaction": {"transactionId": "at1prepared", "serializedTransaction": "{not json"}}})
+ with pytest.raises(CheckpointInvalidError, match="invalid for a prepared Aleo"):
+ recover(b, {**cp, "destination": {"transactionId": "at1d"}})
+
+
+def test_submitted_aleo_source_is_observed_once_never_rebroadcast():
+ b = FakeBridge(ethereum=False)
+ plan, cp = _aleo_eth_checkpoint(b, transactionId="at1burn")
+ progress = recover(b, Checkpoint.from_dict(cp))
+ assert progress.next == "wait" and progress.receipt.status is Status.SOURCE_CONFIRMING
+ b.aleo.confirmed_transactions["at1burn"] = {"status": "accepted"}
+ progress = recover(b, cp)
+ assert progress.next == "wait" and progress.receipt.status is Status.DELIVERY_PENDING
+ assert progress.receipt.source_tx_id == "at1burn" and b.submitted == []
+ # Checkpoint.from_dict (which we do not modify) derives receiptId from source/destination when
+ # absent, and raises its own "no submitted or prepared transaction" error before recover() ever
+ # sees an empty source — so a receiptId must be supplied for recover()'s OWN check to be reached.
+ with pytest.raises(CheckpointInvalidError, match="no submitted source transaction"):
+ recover(b, {**cp, "receiptId": "at1burn", "source": {}})
+ with pytest.raises(CheckpointInvalidError, match="invalid for an Aleo"):
+ recover(b, {**cp, "source": {"transactionId": "at1burn", "approvalTransactionIds": ["0x1"]}})
+
+
+def test_solana_checkpoint_validates_blockhash_pair_and_reads_status():
+ b = FakeBridge(solana=True)
+ plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ cp = create_checkpoint(plan, Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id="sig",
+ protocol_state={"routeId": plan.route_id, "blockhash": "recent",
+ "lastValidBlockHeight": "123456789"}), b.registry)
+ b.sol.source_status_result = Receipt(id="sig", protocol="hyperlane", status=Status.EXPIRED, source_tx_id="sig",
+ protocol_state={"routeId": plan.route_id, "blockhashExpired": True,
+ "sourceError": "Solana transaction expired before confirmation: sig"})
+ progress = recover(b, cp)
+ assert progress.next == "failed" and progress.receipt.status is Status.EXPIRED
+ assert progress.error == "Solana transaction expired before confirmation: sig"
+ assert b.calls == [("sol.source_status", Status.SOURCE_CONFIRMING)]
+ d = cp.to_dict()
+ with pytest.raises(CheckpointInvalidError, match="blockhash"):
+ recover(b, {**d, "source": {"transactionId": "sig", "blockhash": "recent"}})
+ with pytest.raises(CheckpointInvalidError, match="no submitted source transaction"):
+ recover(b, {**d, "source": {"blockhash": "recent", "lastValidBlockHeight": "1"}})
+
+
+def test_evm_hyperlane_delegates_to_eth_recover_source():
+ b = FakeBridge()
+ plan = prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001", recipient=ALEO_RECIPIENT,
+ sender=EVM1)
+ cp = create_checkpoint(plan, Receipt(id="0x" + "11" * 32, protocol="hyperlane", status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": ["0x" + "11" * 32]}),
+ b.registry)
+ b.eth.recover_result = Receipt(id="0x" + "11" * 32, protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": ["0x" + "11" * 32],
+ "sourceSender": EVM1})
+ progress = recover(b, cp)
+ assert progress.next == "resume" and b.calls[0][0] == "eth.recover_source" and b.calls[0][2] is False
+ with pytest.raises(CheckpointInvalidError, match="destination transaction"):
+ recover(b, {**cp.to_dict(), "destination": {"transactionId": "at1x"}})
+
+
+def test_evm_xreserve_recovery_paths():
+ b = FakeBridge(environment="testnet")
+ plan = prepare(b.registry, source="sepolia/usdc", destination="aleo-testnet/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, mint_mode="private", sender=EVM1)
+ base = create_checkpoint(plan, Receipt(id="0x" + "22" * 32, protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="0x" + "22" * 32, protocol_state={"routeId": plan.route_id}),
+ b.registry).to_dict()
+ attested = Receipt(id="0x" + "cc" * 32, protocol="xreserve", status=Status.ATTESTATION_PENDING, source_tx_id="0x" + "22" * 32,
+ protocol_state={"routeId": plan.route_id, "mintMode": "private", "intendedRecipient": ALEO_RECIPIENT,
+ "messageHash": "0x" + "cc" * 32, "nonce": "0x" + "dd" * 32,
+ "bridgeProgram": "test_usdcx_bridge_v2.aleo"})
+ b.eth.recover_result = attested
+ # ATTESTATION_PENDING → one get_status (Circle not ready) → wait
+ assert recover(b, base).next == "wait"
+ assert [c[0] for c in b.calls] == ["eth.recover_source", "xreserve.is_delivered", "xreserve.get_attestation"]
+ # submitted destination tx → DESTINATION_CONFIRMING, observed once
+ b.calls.clear()
+ b.aleo.confirmed_transactions["at1private"] = {"status": "accepted"}
+ done = recover(b, {**base, "destination": {"transactionId": "at1private"}})
+ assert done.next == "done" and done.receipt.destination_tx_id == "at1private"
+ # both prepared and submitted destination → invalid
+ serialized = json.dumps({"type": "execute", "id": "at1mint", "fee": {}})
+ with pytest.raises(CheckpointInvalidError, match="both prepared and submitted"):
+ recover(b, {**base, "destination": {"transactionId": "at1private",
+ "preparedTransaction": {"transactionId": "at1mint", "serializedTransaction": serialized}}})
+ # prepared destination survives only while DESTINATION_ACTION_REQUIRED
+ b.xreserve.delivered_nonces.clear()
+ with pytest.raises(CheckpointInvalidError, match="no longer valid"):
+ recover(b, {**base, "destination": {"preparedTransaction": {"transactionId": "at1mint", "serializedTransaction": serialized}}})
+ from aleo_bridge.types import Attestation
+ b.xreserve.attestations["0x" + "cc" * 32] = Attestation(b"\x00" * 305, bytes.fromhex("cc" * 32), b"\x11" * 65, "complete")
+ ready = recover(b, {**base, "destination": {"preparedTransaction": {"transactionId": "at1mint", "serializedTransaction": serialized}}})
+ assert ready.next == "complete" and ready.receipt.id == "at1mint"
+ assert ready.receipt.protocol_state["preparedDestinationTransaction"] == serialized
+
+
+def test_unsupported_route_and_terminal_cleanup(tmp_path):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ plan, cp = _aleo_eth_checkpoint(b, transactionId="at1burn")
+ store.save(Checkpoint.from_dict(cp))
+ b.aleo.confirmed_transactions["at1burn"] = {"status": "rejected"}
+ progress = recover(b, cp)
+ assert progress.next == "failed" and store.list() == []
+ burn = prepare(b.registry, source="aleo/usdcx", destination="ethereum/usdc", amount="2.1", recipient=EVM1)
+ ok = recover(b, create_checkpoint(burn, Receipt(id="at1b", protocol="xreserve", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="at1b", protocol_state={"routeId": burn.route_id}), b.registry))
+ assert ok.next == "wait"
+
+
+def test_malformed_delivery_verification_raises_checkpoint_invalid():
+ # Item 8 (carried from Task 7 review): a hand-edited/foreign checkpoint whose
+ # deliveryVerification block is missing a key or holds a non-digit value must raise
+ # CheckpointInvalidError, never KeyError, before any network read.
+ b = FakeBridge(ethereum=False)
+ plan, cp = _aleo_eth_checkpoint(b, transactionId="at1burn")
+ with pytest.raises(CheckpointInvalidError, match="destination balance verification"):
+ recover(b, {**cp, "deliveryVerification": {"balanceBeforeAtomic": "100"}}) # missing key
+ with pytest.raises(CheckpointInvalidError, match="destination balance verification"):
+ recover(b, {**cp, "deliveryVerification": {"balanceBeforeAtomic": "100", "expectedIncreaseAtomic": "abc"}})
+ with pytest.raises(CheckpointInvalidError, match="destination balance verification"):
+ recover(b, {**cp, "deliveryVerification": {"balanceBeforeAtomic": None, "expectedIncreaseAtomic": "1"}})
+ assert b.calls == [] and b.events == []
+
+
+def test_terminal_cleanup_deletes_by_checkpoint_id_not_receipt_id(tmp_path):
+ # Item 7 (carried from Task 7 review): a Solana checkpoint whose stored id (the source
+ # signature) differs from the id the refreshed receipt ends up carrying (EXPIRED status
+ # can flip the receipt id to a message id). _finish must delete the record keyed on
+ # cp.id ("sig"), never one keyed on receipt.id ("msg-divergent").
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(solana=True, checkpoints=store)
+ plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ cp = create_checkpoint(plan, Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="sig", protocol_state={"routeId": plan.route_id,
+ "blockhash": "recent",
+ "lastValidBlockHeight": "123456789"}),
+ b.registry)
+ assert cp.id == "sig"
+ store.save(cp)
+ b.sol.source_status_result = Receipt(id="msg-divergent", protocol="hyperlane", status=Status.EXPIRED,
+ source_tx_id="sig",
+ protocol_state={"routeId": plan.route_id,
+ "sourceError": "Solana transaction expired before confirmation: sig"})
+ progress = recover(b, cp)
+ assert progress.next == "failed" and progress.receipt.id == "msg-divergent"
+ assert store.load("sig") is None
+ assert store.load("msg-divergent") is None # nothing was ever stored under this key to begin with
+
+
+def test_recovered_plan_round_trips_through_checkpoint():
+ # Controller ruling (task-7-controller-notes.md #1): _plan_from_intent rebuilds the plan via
+ # prepare(), which is proven field-identical to build_plan for every active route
+ # (tests/test_prepare.py::test_prepare_equals_build_plan_for_every_active_route). Confirm the
+ # round trip (create_checkpoint -> to_dict -> recover's internal from_dict/_plan_from_intent)
+ # holds for an EVM, a Solana and an Aleo-origin route by checking recover()'s Progress.plan.
+ b = FakeBridge(solana=True)
+
+ evm_plan = prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001",
+ recipient=ALEO_RECIPIENT, sender=EVM1)
+ evm_cp = create_checkpoint(evm_plan, Receipt(id="0x" + "11" * 32, protocol="hyperlane",
+ status=Status.SOURCE_APPROVAL_PENDING,
+ protocol_state={"routeId": evm_plan.route_id,
+ "approvalTxIds": ["0x" + "11" * 32]}), b.registry)
+ b.eth.recover_result = Receipt(id="0x" + "11" * 32, protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": evm_plan.route_id, "approvalTxIds": ["0x" + "11" * 32]})
+ assert recover(b, evm_cp).plan == evm_plan
+
+ sol_plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ sol_cp = create_checkpoint(sol_plan, Receipt(id="sig", protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id="sig", protocol_state={"routeId": sol_plan.route_id}),
+ b.registry)
+ assert recover(b, sol_cp).plan == sol_plan
+
+ aleo_plan, aleo_cp = _aleo_eth_checkpoint(b, transactionId="at1burn")
+ assert recover(b, aleo_cp).plan == aleo_plan
diff --git a/bridge-sdk/tests/test_registry.py b/bridge-sdk/tests/test_registry.py
new file mode 100644
index 0000000..114ff24
--- /dev/null
+++ b/bridge-sdk/tests/test_registry.py
@@ -0,0 +1,324 @@
+import re
+
+import pytest
+
+from aleo_bridge import _registry_data as data
+from aleo_bridge.errors import AmbiguousRouteError, ConfigurationError, RouteNotFoundError
+from aleo_bridge.registry import (DEFAULT_REGISTRY, Asset, Chain, Locator, Privacy, Registry, Route,
+ validate_registry)
+
+REG = DEFAULT_REGISTRY
+MAILBOX = {
+ "aleoMailboxStateVerified": True, "aleoMailboxProgram": "hyp_mailbox.aleo", "aleoMailboxProgramEdition": 0,
+ "aleoMailboxLocalDomain": 1634493807, "aleoMailboxObservedNonce": 170, "aleoMailboxObservedProcessCount": 291,
+ "aleoMailboxDefaultIsm": "aleo1yvf5kcsdgnescqq2lar83mms79yh3ugvc3y0mdnlgvx4lyh5zugqr9hptk",
+ "aleoMailboxDefaultHook": "aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74",
+ "aleoMailboxRequiredHook": "aleo1yxevh9qgxehej46j7vueplwjcpfdfml2dje3ey4ukzknx7wzasgqnxgq82",
+ "aleoMailboxDispatchProxy": "aleo1sge9kmjzs3d8fqrscy4hwn7vf9vw4jcxe877lv0m2w8hay78lsxsqg975s",
+ "aleoMailboxOwner": "aleo1ypf8xgvz560ukw25hufj3d77gx69pdcy70nssdfdxd97j80d7cqs98d7x8",
+}
+
+
+def test_shape_and_version():
+ assert REG.version == "2026-08-31.solana-deposits.1"
+ assert len(REG.chains()) == 7 and len(REG.assets()) == 19
+ assert len(REG.routes(include_unavailable=True)) == 22
+ assert len(REG.routes()) == 22 # nothing is 'disabled' in this snapshot; metadata-required stays visible
+ assert validate_registry(REG) is REG
+ assert REG.routes(include_unavailable=True)[0].metadata["xReserveContract"] == "0x8888888199b2Df864bf678259607d6D5EBb4e3Ce"
+
+
+def test_chains():
+ assert [c.id for c in REG.chains()] == ["aleo", "ethereum", "solana", "base", "hyperevm", "aleo-testnet", "sepolia"]
+ assert [c.id for c in REG.chains(environment="testnet")] == ["aleo-testnet", "sepolia"]
+ aleo = REG.chain("aleo")
+ assert (aleo.display_name, aleo.family, aleo.environment, aleo.native_symbol) == ("Aleo", "aleo", "mainnet", "ALEO")
+ assert aleo.protocol_domains == {"xreserve": 10002, "hyperlane": 1634493807}
+ assert REG.chain("ethereum").protocol_domains == {"xreserve": 0, "hyperlane": 1}
+ assert REG.chain("solana").protocol_domains == {"hyperlane": 1399811149}
+ assert REG.chain("base").protocol_domains == {} and REG.chain("hyperevm").native_symbol == "HYPE"
+ assert REG.chain("aleo-testnet").protocol_domains == {"xreserve": 10002, "hyperlane": 1617853565}
+ assert REG.chain("sepolia").protocol_domains == {"hyperlane": 11155111}
+ with pytest.raises(RouteNotFoundError):
+ REG.chain("bitcoin")
+
+
+def test_assets_and_lookups():
+ wbtc = REG.asset("aleo/wbtc")
+ assert wbtc == REG.asset(("aleo", "wbtc")) == REG.asset("ALEO/WBTC")
+ assert (wbtc.key, wbtc.chain_id, wbtc.symbol, wbtc.name, wbtc.decimals, wbtc.kind) == \
+ ("wbtc", "aleo", "WBTC", "Hyperlane WBTC", 8, "token")
+ assert wbtc.locator == Locator("aleo-program", "hyp_warp_token_wbtc_v2.aleo",
+ "aleo1240fsvz2dhmj0cdtt8mc0yc8um9fmu236rqcl2qnlj9703hd2vpsdwyrtf")
+ assert wbtc.privacy == Privacy("arc20", "arc20_wbtc.aleo")
+ assert wbtc.address_regex == "^aleo1[0-9a-z]{58}$"
+ usdcx = REG.asset("aleo/usdcx")
+ assert usdcx.locator == Locator("aleo-program", "usdcx_stablecoin.aleo") and usdcx.privacy == Privacy("arc22", "usdcx_stablecoin.aleo")
+ assert REG.asset("aleo/aleo").privacy is None and REG.asset("aleo/usad").locator == Locator("aleo-program", "usad_stablecoin.aleo")
+ assert REG.asset("ethereum/usdc").locator == Locator("evm-contract", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
+ assert REG.asset("ethereum/eth").locator == Locator("native", "ETH") and REG.asset("ethereum/eth").kind == "native"
+ assert REG.asset("ethereum/wbtc").locator.value == "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
+ assert REG.asset("ethereum/usdt").locator.value == "0xdAC17F958D2ee523a2206206994597C13D831ec7"
+ assert REG.asset("sepolia/usdc").locator.value == "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
+ assert REG.asset("aleo-testnet/usdcx").privacy == Privacy("arc22", "test_usdcx_stablecoin.aleo")
+ for no_locator in ("ethereum/aleo", "ethereum/usad", "solana/aleo", "base/aleo", "hyperevm/aleo"):
+ assert REG.asset(no_locator).locator is None
+ assert REG.asset("solana/sol").address_regex == "^[1-9A-HJ-NP-Za-km-z]{32,44}$"
+ assert REG.asset("ethereum/usdc").matches_address("0x0000000000000000000000000000000000000001")
+ assert not REG.asset("aleo/usdcx").matches_address("0xabc")
+ # re.fullmatch, not re.search: a trailing newline must not sneak past the "$" anchor
+ assert not REG.asset("ethereum/usdc").matches_address("0x0000000000000000000000000000000000000001\n")
+ assert [a.id for a in REG.assets(chain="aleo")] == ["aleo/aleo", "aleo/usdcx", "aleo/eth", "aleo/wbtc", "aleo/usdt", "aleo/sol", "aleo/usad"]
+ assert [a.id for a in REG.assets(symbol="aleo")] == ["aleo/aleo", "ethereum/aleo", "solana/aleo", "base/aleo", "hyperevm/aleo"]
+ assert [a.id for a in REG.assets(environment="testnet")] == ["aleo-testnet/usdcx", "sepolia/usdc"]
+ for bad in ("aleo/doge", "doge", ("aleo", "doge"), "a/b/c"):
+ with pytest.raises(RouteNotFoundError):
+ REG.asset(bad)
+
+
+def test_usdcx_only_via_xreserve_and_others_via_hyperlane():
+ usdcx = [r for r in REG.routes(include_unavailable=True) if "usdcx" in r.source_asset_id or "usdcx" in r.destination_asset_id]
+ assert usdcx and all(r.protocol == "xreserve" for r in usdcx)
+ for symbol in ("ETH", "WBTC", "USDT", "SOL", "ALEO", "USAD"):
+ routes = REG.routes(symbol=symbol, include_unavailable=True)
+ assert routes and all(r.protocol == "hyperlane" for r in routes), symbol
+
+
+def test_xreserve_routes():
+ xr = REG.routes(protocol="xreserve", include_unavailable=True)
+ assert [r.id for r in xr] == ["xreserve:ethereum/usdc->aleo/usdcx", "xreserve:aleo/usdcx->ethereum/usdc",
+ "xreserve:sepolia/usdc->aleo-testnet/usdcx", "xreserve:aleo-testnet/usdcx->sepolia/usdc"]
+ assert all(r.availability == "active" and r.active for r in xr)
+ assert all(r.metadata["ethereumDestinationDomain"] == 0 and r.metadata["arcDestinationDomain"] == 26 for r in xr)
+ assert all(r.source == "https://developers.circle.com/xreserve/references/supported-blockchains-and-domains" for r in xr)
+ main = REG.route("xreserve:aleo/usdcx->ethereum/usdc").metadata
+ assert main == {
+ "xReserveContract": "0x8888888199b2Df864bf678259607d6D5EBb4e3Ce", "sourceChainId": 1, "sourceDomain": 0,
+ "ethereumDestinationDomain": 0, "arcDestinationDomain": 26, "remoteDomain": 10002,
+ "remoteToken": "usdcx_stablecoin.aleo",
+ "remoteTokenBytes32": "0x11ea7dab1d29d5f61500582c63e98c42e1165f9ba050ea9d0c6af9f871987711",
+ "minimumAmountAtomic": "2000000", "withdrawalFeeAtomic": "2000000", "maxFeeAtomic": "100000",
+ "bridgeProgram": "usdcx_bridge_v2.aleo", "wrapperProgram": "shielded_usdcx_wrapper.aleo",
+ "attestationBaseUrl": "https://xreserve-api.circle.com/v1/attestations",
+ }
+ test = REG.route("xreserve:sepolia/usdc->aleo-testnet/usdcx").metadata
+ assert test["xReserveContract"] == "0x008888878f94C0d87defdf0B07f46B93C1934442" and test["sourceChainId"] == 11155111
+ assert test["remoteToken"] == "test_usdcx_stablecoin.aleo" and test["bridgeProgram"] == "test_usdcx_bridge_v2.aleo"
+ assert test["remoteTokenBytes32"] == "0xb143ed52c774cd1d4a519d0e796f15916be5a9e1d45edcd9852dd23f68f53401"
+ assert test["attestationBaseUrl"] == "https://xreserve-api-testnet.circle.com/v1/attestations"
+ assert REG.route("xreserve:ethereum/usdc->aleo/usdcx").deployment_id == "xreserve-usdcx-aleo"
+ assert REG.route("xreserve:sepolia/usdc->aleo-testnet/usdcx").deployment_id == "xreserve-usdcx-aleo-testnet"
+
+
+def test_inbound_ethereum_hyperlane_routes():
+ inbound = [REG.route(i) for i in ("hyperlane:ethereum/eth->aleo/eth", "hyperlane:ethereum/wbtc->aleo/wbtc", "hyperlane:ethereum/usdt->aleo/usdt")]
+ assert all(r.active for r in inbound)
+ for r in inbound:
+ m = r.metadata
+ assert m["registryCommit"] == "2621c16f2db1ccb46643265c110dac5ca2c7c51a"
+ assert m["sourceChainId"] == 1 and m["destinationDomain"] == 1634493807
+ assert m["mailboxAddress"] == "0xc005dc82818d67AF737725bD4bf75435d065D239"
+ assert m["interchainGasPaymaster"] == "0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611"
+ assert m["interchainSecurityModule"] == "0x0000000000000000000000000000000000000000"
+ for k, v in MAILBOX.items():
+ assert m[k] == v
+ assert r.source == "https://github.com/hyperlane-xyz/hyperlane-registry/tree/2621c16f2db1ccb46643265c110dac5ca2c7c51a/deployments/warp_routes"
+ eth, wbtc, usdt = (r.metadata for r in inbound)
+ assert (eth["routerAddress"], eth["routerType"]) == ("0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A", "native")
+ assert eth["destinationRouter"] == "hyp_warp_token_eth_v2.aleo/aleo1t7f29tq9qng2lfvrkpcuvu59jn24hrmzqdyqfn6p0u5p80npfvqqecmkj8"
+ assert (wbtc["routerAddress"], wbtc["routerType"], wbtc["tokenAddress"]) == \
+ ("0x20CDC85778b732073F7EecEF3DF25c0d310f8772", "collateral", "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599")
+ assert (usdt["routerAddress"], usdt["tokenAddress"], usdt["requiresApprovalReset"]) == \
+ ("0x3C2064D78e4578E8F936E3db42aEF044E33FBF31", "0xdAC17F958D2ee523a2206206994597C13D831ec7", True)
+ assert "requiresApprovalReset" not in wbtc
+
+
+def test_aleo_origin_withdrawals_are_active_and_pinned():
+ ids = ["hyperlane:aleo/eth->ethereum/eth", "hyperlane:aleo/wbtc->ethereum/wbtc",
+ "hyperlane:aleo/usdt->ethereum/usdt", "hyperlane:aleo/sol->solana/sol"]
+ routes = [REG.route(i) for i in ids]
+ assert all(r.active for r in routes)
+ assert all(r.metadata["aleoPlaceholderConfiguration"] is False for r in routes)
+ assert all(r.metadata["aleoWithdrawalReviewedAt"] == "2026-08-26" for r in routes)
+ assert all(r.metadata["aleoHookManagerProgram"] == "hyp_hook_manager.aleo" for r in routes)
+ assert [r.metadata["aleoRouterProgram"] for r in routes] == [
+ "hyp_warp_token_eth_v2.aleo", "hyp_warp_token_wbtc_v2.aleo", "hyp_warp_token_usdt_v2.aleo", "hyp_warp_token_sol_v2.aleo"]
+ for r in routes:
+ m = r.metadata
+ assert m["aleoAppMetadataVerified"] is True and m["aleoRemoteRouterVerified"] is True
+ assert m["aleoAllowanceSpendersVerified"] is True and m["aleoUnusedAllowancesVerified"] is True
+ assert m["aleoTokenType"] == "1"
+ assert m["aleoIsm"] == m["aleoHook"] == "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
+ assert m["aleoAllowanceSpender0"] == "aleo194tz0jmyq8rd9htvnqppqw4jqerk2p2zd8plzn3sxl06wcgsm5pq9fka74"
+ assert m["aleoAllowanceSpender1"] == m["aleoAllowanceSpender2"] == m["aleoAllowanceSpender3"] == \
+ "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
+ assert m["aleoAllowanceAmount1"] == m["aleoAllowanceAmount2"] == m["aleoAllowanceAmount3"] == "0"
+ # slot 0's pinned "0" is a placeholder; the live IGP quote is fetched at execution time (out of
+ # scope for the registry), matching veil's default.ts/dist behavior (placeholderFields mechanism).
+ assert m["aleoAllowanceAmount0"] == "0"
+ for k, v in MAILBOX.items():
+ assert m[k] == v
+
+
+def test_eth_wbtc_usdt_sol_metadata_literals():
+ eth = REG.route("hyperlane:aleo/eth->ethereum/eth").metadata
+ assert eth["aleoTokenOwner"] == "aleo1wq6f6qdqya44avznygz5hae40u3mjg64w0r93a4qfu4utpf8cg9q566f4r"
+ assert eth["aleoTokenId"] == "133188123661477349522757068766864658505569365361420630212878794317749195359field"
+ assert (eth["aleoLocalDecimals"], eth["aleoRemoteDecimals"], eth["aleoProgramEdition"], eth["aleoDestinationDomain"]) == (18, 18, 0, 1)
+ assert eth["aleoRemoteRouterEvmAddress"] == "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A" and eth["aleoRemoteRouterGas"] == "44000"
+ assert eth["aleoRemoteRouterRecipient"] == "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 56u8, 212u8, 71u8, 105u8, 79u8, 92u8, 31u8, 119u8, 58u8, 227u8, 19u8, 44u8, 249u8, 59u8, 243u8, 11u8, 126u8, 193u8, 250u8, 90u8]"
+ assert eth["aleoSampleTransferSource"] == "https://explorer.provable.com/transaction/at1vu0yckkms887zkl3qz7plnncd56jtf5zeal4uj2808upsjkusy8q7yp9v8"
+ assert eth["routerAddress"] == "0x38D447694f5c1f773ae3132cf93bF30B7Ec1Fa5A" # ETH_HYPERLANE_METADATA is merged into the reverse route too
+
+ wbtc = REG.route("hyperlane:aleo/wbtc->ethereum/wbtc").metadata
+ assert wbtc["aleoTokenOwner"] == "aleo14jauje2a5sncm9u5t3mt6qqv3eq2hatkddskccs0dvsy35a0x58q0d6f95"
+ assert wbtc["aleoTokenId"] == "1505227928464760254508513036497943623956572091841806589002910775534260084309field"
+ assert (wbtc["aleoLocalDecimals"], wbtc["aleoRemoteDecimals"], wbtc["aleoProgramEdition"]) == (8, 8, 0)
+ assert wbtc["aleoRemoteRouterRecipient"] == "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 32u8, 205u8, 200u8, 87u8, 120u8, 183u8, 50u8, 7u8, 63u8, 126u8, 236u8, 239u8, 61u8, 242u8, 92u8, 13u8, 49u8, 15u8, 135u8, 114u8]"
+ assert wbtc["aleoRemoteRouterGas"] == "68000"
+ assert wbtc["aleoProgramSource"] == "https://explorer.provable.com/program/hyp_warp_token_wbtc_v2.aleo"
+ assert wbtc["aleoAppMetadataSource"] == "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_wbtc_v2.aleo/mapping/app_metadata/true"
+ assert wbtc["aleoRemoteRouterSource"] == "https://api.explorer.provable.com/v2/mainnet/program/hyp_warp_token_wbtc_v2.aleo/mapping/remote_routers/1u32"
+ assert wbtc["aleoAppMetadataReviewedAt"] == wbtc["aleoRemoteRouterReviewedAt"] == "2026-08-17"
+
+ usdt = REG.route("hyperlane:aleo/usdt->ethereum/usdt").metadata
+ assert usdt["aleoTokenOwner"] == "aleo1l3gwacmjruxryy9c7c4fn0acyzprf29hucrvthw7f63lpyhd5y9srydq8z"
+ assert usdt["aleoTokenId"] == "8295938150000417034830036849466229528602563851235385582732969109393809606969field"
+ assert (usdt["aleoLocalDecimals"], usdt["aleoRemoteDecimals"], usdt["aleoProgramEdition"], usdt["aleoScale"]) == (6, 18, 1, "1000000000000")
+ assert usdt["aleoRemoteRouterRecipient"] == "[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 60u8, 32u8, 100u8, 215u8, 142u8, 69u8, 120u8, 232u8, 249u8, 54u8, 227u8, 219u8, 66u8, 174u8, 240u8, 68u8, 227u8, 63u8, 191u8, 49u8]"
+ assert usdt["aleoRemoteRouterGas"] == "68000" and usdt["aleoSampleTransferDestinationDomain"] == 56
+ assert usdt["aleoSampleTransferSource"] == "https://explorer.provable.com/transaction/at19caeeee8v3xc4kfwen4tx89f0tnggrpjp0anrhq2ca3y82xr9q8qyz8a9r"
+ assert usdt["aleoHyperlaneConfigSource"] == "https://github.com/hyperlane-xyz/hyperlane-registry/blob/418056e21734d26a7d14692e0ec5e902cc9e86bf/deployments/warp_routes/USDT/aleo-config.yaml"
+
+ sol = REG.route("hyperlane:aleo/sol->solana/sol").metadata
+ assert sol["aleoTokenOwner"] == "aleo1wr8rfr4ggedjxtg5e23s38zqkgy2j05uc9l8t4akjp5zcw3levpswkwk45"
+ assert sol["aleoTokenId"] == "6148061383892805373029428966764338809222769879628268522058032128225601478383field"
+ assert (sol["aleoLocalDecimals"], sol["aleoRemoteDecimals"], sol["aleoDestinationDomain"]) == (9, 9, 1399811149)
+ assert sol["aleoRemoteRouterSolanaAddress"] == "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7"
+ assert sol["aleoRemoteRouterRecipient"] == "[112u8, 4u8, 72u8, 22u8, 219u8, 143u8, 68u8, 202u8, 21u8, 197u8, 236u8, 182u8, 198u8, 142u8, 52u8, 96u8, 142u8, 38u8, 51u8, 113u8, 116u8, 143u8, 96u8, 123u8, 104u8, 126u8, 97u8, 73u8, 7u8, 6u8, 211u8, 122u8]"
+ assert sol["aleoRemoteRouterGas"] == "300000"
+ assert sol["aleoSampleTransitionId"] == "au15fg39h53h55tkj0nexrme3k6pvgxngxapcyajdhf06jcg3cyeugq5kd7hg"
+ assert "routerAddress" not in sol # no Ethereum common block on the SOL withdrawal
+
+
+def test_solana_deposit_route_metadata():
+ r = REG.route("hyperlane:solana/sol->aleo/sol")
+ assert r.active and r.deployment_id == "SOL/aleo"
+ expected = {
+ "warpProgramAddress": "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7", "tokenPda": "JDkpV5CsSbhyGhHhirC5DjGPTcuKWUVHtBZ5MFsgu3ZW",
+ "nativeCollateralPda": "8HY3hxmnrWwqEmcdwkSnfN9wEQFUkyiwZvU1vMbnXgbC", "dispatchAuthorityPda": "ATDttjggAZKyS19kcV6Rn56oMi49gDprZGckRou9vkkY",
+ "mailboxProgramAddress": "E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi", "mailboxOutboxPda": "BvZpTuYLAR77mPhH4GtvwEWUTs53GQqkgBNuXpCePVNk",
+ "igpProgramAddress": "BhNcatUDC2D5JTyeaqrdSukiVFsEHK7e3hVmKMztwefv", "igpProgramDataPda": "8Cv4PHJ6Cf3xY7dse7wYeZKtuQv9SAN6ujt5w22a2uho",
+ "igpAccount": "JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M", "igpOverheadAccount": "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF",
+ "splNoopProgramAddress": "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV", "destinationDomain": 1634493807,
+ "destinationGasAmount": "464000", "registryCommit": "418056e21734d26a7d14692e0ec5e902cc9e86bf", "solanaReviewedAt": "2026-08-31",
+ "solanaConfigSource": "https://github.com/hyperlane-xyz/hyperlane-registry/blob/418056e21734d26a7d14692e0ec5e902cc9e86bf/deployments/warp_routes/SOL/aleo-config.yaml",
+ }
+ for k, v in {**expected, **MAILBOX}.items():
+ assert r.metadata[k] == v, k
+
+
+def test_metadata_required_routes():
+ pairs = [("aleo/aleo", "ethereum/aleo"), ("aleo/aleo", "solana/aleo"), ("aleo/aleo", "base/aleo"), ("aleo/aleo", "hyperevm/aleo")]
+ for left, right in pairs:
+ for rid in (f"hyperlane:{left}->{right}", f"hyperlane:{right}->{left}"):
+ r = REG.route(rid)
+ assert r.availability == "metadata-required" and not r.active and r.deployment_id == "ALEO/aleo"
+ assert dict(r.metadata) == MAILBOX | {
+ "aleoHookManagerProgram": "hyp_hook_manager.aleo",
+ "aleoHookManagerProgramSource": "https://explorer.provable.com/program/hyp_hook_manager.aleo",
+ "aleoMailboxProgramSource": "https://explorer.provable.com/program/hyp_mailbox.aleo",
+ "aleoMailboxMetadataSource": "https://api.explorer.provable.com/v2/mainnet/program/hyp_mailbox.aleo/mapping/mailbox/true",
+ "aleoMailboxMetadataReviewedAt": "2026-08-17",
+ }
+ usad_in = REG.route("hyperlane:ethereum/usad->aleo/usad")
+ assert usad_in.availability == "metadata-required" and usad_in.deployment_id == "USAD/aleo"
+ usad_out = REG.route("hyperlane:aleo/usad->ethereum/usad")
+ m = usad_out.metadata
+ assert usad_out.availability == "metadata-required" and m["aleoPlaceholderConfiguration"] is True
+ assert m["aleoRouterProgram"] == "hyp_warp_token_usad_v2.aleo" and m["aleoDestinationDomain"] == 1
+ assert m["aleoTokenType"] == "0" and m["aleoTokenId"] == "0field" and m["aleoRemoteRouterGas"] == "0"
+ assert m["aleoTokenOwner"] == m["aleoIsm"] == m["aleoHook"] == "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+ assert m["aleoRemoteRouterRecipient"] == "[" + ", ".join(["0u8"] * 32) + "]" and m["aleoRecipient"] == "[0u128, 0u128]"
+ assert all(m[f"aleoAllowanceAmount{i}"] == "0" for i in range(4))
+
+
+def test_route_filters_and_find_route():
+ assert [r.id for r in REG.routes(source="aleo", protocol="xreserve")] == ["xreserve:aleo/usdcx->ethereum/usdc"]
+ assert [r.id for r in REG.routes(source="aleo/wbtc")] == ["hyperlane:aleo/wbtc->ethereum/wbtc"]
+ assert [r.id for r in REG.routes(destination="aleo", symbol="wbtc")] == ["hyperlane:ethereum/wbtc->aleo/wbtc"]
+ assert len(REG.routes(environment="testnet")) == 2 and len(REG.routes(environment="mainnet")) == 20
+ assert len(REG.routes(source="solana")) == 2 # SOL deposit + metadata-required ALEO
+ assert REG.find_route("aleo/wbtc", "ethereum/wbtc").id == "hyperlane:aleo/wbtc->ethereum/wbtc"
+ assert REG.find_route(("ethereum", "usdc"), ("aleo", "usdcx"), protocol="xreserve").id == "xreserve:ethereum/usdc->aleo/usdcx"
+ assert REG.find_route("aleo/usad", "ethereum/usad").availability == "metadata-required" # visible, refused later
+ with pytest.raises(RouteNotFoundError):
+ REG.find_route("aleo/wbtc", "solana/sol")
+ with pytest.raises(RouteNotFoundError):
+ REG.find_route("aleo/wbtc", "ethereum/wbtc", protocol="xreserve")
+ with pytest.raises(RouteNotFoundError):
+ REG.route("hyperlane:aleo/doge->ethereum/doge")
+ # A synthetic duplicate pair across protocols is ambiguous without protocol=
+ dup = Route("xreserve:aleo/wbtc->ethereum/wbtc", "xreserve", "mainnet", "aleo/wbtc", "ethereum/wbtc", "active", None, None, {})
+ reg2 = Registry(REG.version, REG.chains(), REG.assets(), [*REG.routes(include_unavailable=True), dup])
+ with pytest.raises(AmbiguousRouteError):
+ reg2.find_route("aleo/wbtc", "ethereum/wbtc")
+ assert reg2.find_route("aleo/wbtc", "ethereum/wbtc", protocol="hyperlane").protocol == "hyperlane"
+ # disabled routes are hidden from routes() and find_route() unless include_unavailable
+ off = Route("hyperlane:aleo/eth->ethereum/eth", "hyperlane", "mainnet", "aleo/eth", "ethereum/eth", "disabled", None, None, {})
+ reg3 = Registry(REG.version, REG.chains(), REG.assets(), [off])
+ assert reg3.routes() == [] and reg3.routes(include_unavailable=True) == [off]
+ with pytest.raises(RouteNotFoundError):
+ reg3.find_route("aleo/eth", "ethereum/eth")
+
+
+def test_route_meta_helpers():
+ r = REG.route("hyperlane:aleo/eth->ethereum/eth")
+ assert r.meta_str("aleoRouterProgram") == "hyp_warp_token_eth_v2.aleo"
+ assert r.meta_int("aleoDestinationDomain") == 1 and r.meta_int("missing", 7) == 7
+ with pytest.raises(ConfigurationError, match="aleoNope is missing"):
+ r.meta_str("aleoNope")
+ with pytest.raises(ConfigurationError, match="invalid"):
+ r.meta_int("aleoMailboxStateVerified") # bool is not an int here
+
+
+def _with(**overrides) -> Registry:
+ base = dict(version=REG.version, chains=REG.chains(), assets=REG.assets(), routes=REG.routes(include_unavailable=True))
+ base.update(overrides)
+ return Registry(base["version"], base["chains"], base["assets"], base["routes"])
+
+
+def test_validation_failures():
+ from dataclasses import replace
+ r0 = REG.routes(include_unavailable=True)[0]
+ with pytest.raises(ConfigurationError, match="unknown source asset missing/asset"):
+ validate_registry(_with(routes=[replace(r0, source_asset_id="missing/asset")]))
+ with pytest.raises(ConfigurationError, match="Duplicate bridge route id"):
+ validate_registry(_with(routes=[r0, r0]))
+ a0 = REG.assets()[0]
+ with pytest.raises(ConfigurationError, match="Duplicate bridge asset key"):
+ validate_registry(_with(assets=[a0, replace(a0, id=a0.id + "-duplicate")], routes=[]))
+ with pytest.raises(ConfigurationError, match="invalid address validation regex"):
+ validate_registry(_with(assets=[replace(a0, address_regex="[")], routes=[]))
+ usdc = REG.asset("ethereum/usdc")
+ with pytest.raises(ConfigurationError, match="privacy capability on a non-Aleo chain"):
+ validate_registry(_with(assets=[replace(a, privacy=Privacy("arc20", "arc20_usdc.aleo")) if a.id == usdc.id else a for a in REG.assets()]))
+ with pytest.raises(ConfigurationError, match="empty privacy program"):
+ validate_registry(_with(assets=[replace(a, privacy=Privacy("arc20", "")) if a.id == "aleo/sol" else a for a in REG.assets()]))
+ with pytest.raises(ConfigurationError, match="crosses registry environments"):
+ validate_registry(_with(routes=[replace(r0, environment="testnet")]))
+ with pytest.raises(ConfigurationError, match="missing required Solana Hyperlane metadata"):
+ sol = REG.route("hyperlane:solana/sol->aleo/sol")
+ incomplete = {k: v for k, v in sol.metadata.items() if k != "igpAccount"}
+ validate_registry(_with(routes=[replace(r, metadata=incomplete) if r.id == sol.id else r for r in REG.routes(include_unavailable=True)]))
+ with pytest.raises(ConfigurationError, match="Duplicate bridge chain id"):
+ validate_registry(_with(chains=[*REG.chains(), REG.chain("aleo")]))
+ with pytest.raises(ConfigurationError, match="version must not be empty"):
+ validate_registry(_with(version=" "))
+
+
+def test_data_module_is_plain_literals():
+ assert data.REGISTRY_VERSION == REG.version
+ assert len(data.CHAINS) == 7 and len(data.ASSETS) == 19 and len(data.ROUTES) == 22
+ assert all(isinstance(c, dict) for c in data.CHAINS) and all(isinstance(r["metadata"], dict) for r in data.ROUTES)
+ assert re.compile(data.ALEO_ADDRESS).match("aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n")
diff --git a/bridge-sdk/tests/test_resume_complete.py b/bridge-sdk/tests/test_resume_complete.py
new file mode 100644
index 0000000..a54670a
--- /dev/null
+++ b/bridge-sdk/tests/test_resume_complete.py
@@ -0,0 +1,389 @@
+"""``lifecycle.resume`` and ``lifecycle.complete`` — the two caller-boundary verbs.
+
+``resume`` continues an interrupted SOURCE leg: an Aleo leg rebroadcasts the checkpointed bytes
+byte-for-byte (a duplicate answer means the first broadcast won and counts as success), an EVM leg
+re-scans source history first and only authorizes the one step history proves is still missing.
+``complete`` submits the single user-signed Aleo destination transaction a private USDCx mint
+needs, and is idempotent through the same rebroadcast rule.
+
+The funds-critical invariants under test: neither verb ever repeats an irreversible step, veil's
+two resume guards (hook-data commitment, surviving allowance) refuse rather than guess, a private
+resume/complete without its ``secret_nonce`` is refused before any RPC, and the secret nonce never
+reaches a checkpoint, a protocol_state, or a Progress.
+"""
+import json
+
+import pytest
+from aleo import AleoNetworkError
+
+from aleo_bridge._calls import is_duplicate_submission
+from aleo_bridge.checkpoint import FileCheckpointStore
+from aleo_bridge.errors import (AttestationError, CheckpointInvalidError, ConfigurationError,
+ NotResumableError)
+from aleo_bridge.lifecycle import (complete, is_duplicate_broadcast_error, prepare, resume,
+ submit_serialized)
+from aleo_bridge.types import Receipt, Status, to_progress
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, EVM_ADDRESS, SOL_ADDRESS, FakeBridge
+from tests.test_get_status import SIG, _inbound_private
+
+APPROVAL = "0x" + "11" * 32
+
+
+def _prepared_progress(b):
+ """An Aleo-origin transfer proved but never broadcast — exactly what ``execute`` checkpoints
+ between ``delegate_prepared`` and ``submit_prepared``."""
+ plan = prepare(b.registry, source="aleo/eth", destination="ethereum/eth",
+ amount="0.000000000000000001", recipient=EVM_ADDRESS)
+ serialized = json.dumps({"type": "execute", "id": "at1prepared", "fee": {}})
+ receipt = Receipt(id="at1prepared", protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "preparedTransaction": serialized,
+ "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"})
+ return plan, serialized, to_progress(plan, receipt)
+
+
+def _xreserve_progress(b, **plan_kw):
+ plan = prepare(b.registry, source="ethereum/usdc", destination="aleo/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, sender=EVM_ADDRESS, **plan_kw)
+ receipt = Receipt(id=APPROVAL, protocol="xreserve", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [APPROVAL],
+ "sourceSender": EVM_ADDRESS,
+ "hookData": "0x" + b.eth.hook_data.hex()})
+ return plan, receipt
+
+
+# ── duplicate-broadcast classification (plan 1's rule, not a second one) ──────
+
+def test_duplicate_detection_is_plan_1s_rule_and_refuses_double_spend_shapes():
+ assert is_duplicate_broadcast_error is is_duplicate_submission
+ assert is_duplicate_broadcast_error(AleoNetworkError("Transaction 'at1x' already exists in the ledger", status=400))
+ assert is_duplicate_broadcast_error(AleoNetworkError("transaction at1x already exists in the memory pool"))
+ # a DIFFERENT transaction colliding with this one's records is a real failure, never success
+ assert not is_duplicate_broadcast_error(AleoNetworkError("Duplicate serial number found in transaction"))
+ assert not is_duplicate_broadcast_error(AleoNetworkError("duplicate output id in transaction"))
+ assert not is_duplicate_broadcast_error(AleoNetworkError("Duplicate transaction at1x"))
+ assert not is_duplicate_broadcast_error(AleoNetworkError("Invalid transaction: fee verification failed", status=400))
+
+
+def test_submit_serialized_refuses_an_id_the_node_did_not_echo():
+ b = FakeBridge(ethereum=False)
+ serialized = json.dumps({"type": "execute", "id": "at1prepared", "fee": {}})
+ assert submit_serialized(b, serialized, "at1prepared") == "at1prepared"
+ b.aleo.network.submit_transaction = lambda tx: "at1other"
+ with pytest.raises(CheckpointInvalidError, match="expected at1prepared"):
+ submit_serialized(b, serialized, "at1prepared")
+
+
+# ── resume: Aleo source ───────────────────────────────────────────────────────
+
+def test_resume_rebroadcasts_identical_bytes_and_treats_a_duplicate_as_success():
+ b = FakeBridge(ethereum=False)
+ plan, serialized, progress = _prepared_progress(b)
+ cps = []
+ out = resume(b, progress, on_checkpoint=cps.append)
+ assert b.aleo.submitted == [serialized] # byte-for-byte, never re-proved
+ assert out.next == "wait" and out.receipt.status is Status.SOURCE_CONFIRMING
+ assert out.receipt.source_tx_id == "at1prepared"
+ assert out.receipt.protocol_state == {"routeId": plan.route_id,
+ "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"} # bytes discarded
+ assert len(cps) == 1 and cps[0].source == {"transactionId": "at1prepared"}
+ assert cps[0].delivery_verification == {"balanceBeforeAtomic": "100", "expectedIncreaseAtomic": "1"}
+
+ # the node says it already knows this transaction: the earlier broadcast won the race
+ b2 = FakeBridge(ethereum=False)
+ b2.aleo.duplicate_on_submit = True
+ assert resume(b2, progress).receipt.status is Status.SOURCE_CONFIRMING
+
+ # any other node rejection is a real failure and propagates
+ b3 = FakeBridge(ethereum=False)
+
+ def invalid(tx):
+ raise AleoNetworkError("Invalid transaction: fee verification failed", status=400)
+
+ b3.aleo.network.submit_transaction = invalid
+ with pytest.raises(AleoNetworkError):
+ resume(b3, progress)
+
+ # a node that answers with a different id never gets recorded as this transfer's transaction
+ b4 = FakeBridge(ethereum=False)
+ b4.aleo.network.submit_transaction = lambda tx: "at1other"
+ with pytest.raises(CheckpointInvalidError, match="expected at1prepared"):
+ resume(b4, progress)
+
+
+def test_resume_replaces_the_prepared_checkpoint_in_the_bound_store(tmp_path):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(ethereum=False, checkpoints=store)
+ plan, serialized, progress = _prepared_progress(b)
+ from aleo_bridge.checkpoint import create_checkpoint
+ store.save(create_checkpoint(plan, progress.receipt, b.registry))
+ assert store.list()[0].source["preparedTransaction"]["serializedTransaction"] == serialized
+ resume(b, progress)
+ saved = store.list()
+ assert [c.id for c in saved] == ["at1prepared"]
+ assert saved[0].source == {"transactionId": "at1prepared"} # the unbroadcast bytes are gone
+
+
+def test_resume_refuses_a_wrong_state_a_foreign_route_and_mismatched_bytes():
+ b = FakeBridge(ethereum=False)
+ plan, serialized, progress = _prepared_progress(b)
+ with pytest.raises(NotResumableError, match="SOURCE_SUBMISSION_PENDING"):
+ resume(b, to_progress(plan, progress.receipt.replace(status=Status.SOURCE_CONFIRMING)))
+ with pytest.raises(NotResumableError, match="serialized transaction"):
+ resume(b, to_progress(plan, progress.receipt.replace(protocol_state={"routeId": plan.route_id})))
+ with pytest.raises(CheckpointInvalidError, match="id does not match"):
+ resume(b, to_progress(plan, progress.receipt.replace(id="at1else")))
+ with pytest.raises(CheckpointInvalidError, match="does not match the prepared route"):
+ resume(b, to_progress(plan, progress.receipt.replace(
+ protocol_state={**progress.receipt.protocol_state, "routeId": "other"})))
+ assert b.aleo.submitted == []
+
+
+def test_resume_refuses_a_solana_source_leg_and_points_at_recover():
+ """``SolCall`` has no approval step and no resumable pre-broadcast state: there is nothing to
+ continue, so resume never guesses — it sends the caller to recover()/wait()."""
+ b = FakeBridge(solana=True)
+ plan = prepare(b.registry, source="solana/sol", destination="aleo/sol", amount="0.000000001",
+ recipient=ALEO_RECIPIENT, sender=SOL_ADDRESS)
+ sig = "5igNature" * 8
+ receipt = Receipt(id=sig, protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id})
+ with pytest.raises(NotResumableError, match="recover"):
+ resume(b, to_progress(plan, receipt))
+ assert b.calls == []
+
+
+# ── resume: EVM source ────────────────────────────────────────────────────────
+
+def test_resume_evm_xreserve_rescans_then_deposits_once():
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ b.eth.recover_result = receipt # history has no deposit → still submission pending
+ cps = []
+ out = resume(b, to_progress(plan, receipt), secret_nonce="7scalar", on_checkpoint=cps.append)
+ assert [c[0] for c in b.calls] == ["eth.recover_source", "eth.quote_deposit_usdc", "eth.deposit_usdc"]
+ assert b.calls[0][2] is True # required=True: refuse to guess without an approval block
+ assert b.calls[1][1] == {"plan": plan, "secret_nonce": "7scalar"}
+ assert b.calls[2][1] == {"plan": plan, "secret_nonce": "7scalar"}
+ assert out.receipt.status is Status.ATTESTATION_PENDING
+ assert out.receipt.protocol_state["approvalTxIds"] == [APPROVAL] # prior approval carried forward
+ # the secret nonce reaches the module and nothing else
+ assert "7scalar" not in json.dumps(out.receipt.protocol_state)
+ assert all("7scalar" not in json.dumps(c.to_dict()) for c in cps)
+
+
+def test_resume_evm_never_repeats_a_deposit_history_already_contains():
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ b.eth.recover_result = receipt.replace(status=Status.ATTESTATION_PENDING, id="0x" + "cc" * 32)
+ out = resume(b, to_progress(plan, receipt), secret_nonce="7scalar")
+ assert [c[0] for c in b.calls] == ["eth.recover_source"]
+ assert out.receipt.status is Status.ATTESTATION_PENDING and out.next == "wait"
+
+
+def test_resume_refuses_when_the_re_quoted_hook_data_does_not_match_the_checkpoint():
+ """veil guard 1: the hook commits ``(recipient, secret_nonce)``. A deposit built with a
+ different nonce than the approval was quoted against mints to a commitment nobody can open."""
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ b.eth.recover_result = receipt
+ b.eth.hook_data = bytes([2]) + b"\x99" * 64
+ with pytest.raises(NotResumableError, match="secret nonce"):
+ resume(b, to_progress(plan, receipt), secret_nonce="7scalar")
+ assert "eth.deposit_usdc" not in [c[0] for c in b.calls]
+
+
+@pytest.mark.parametrize("hook", [None, "not-hex", "0x", "0x" + "11" * 64, "0x" + "11" * 66, 65])
+def test_resume_refuses_an_xreserve_checkpoint_without_usable_hook_data(hook):
+ """Fix round 1 (R1): with no checkpointed hook there is nothing to compare the re-quote
+ against, so the equality guard below would pass vacuously and the deposit could be re-hooked to
+ a different commitment. Missing, malformed or wrong-width hook data is refused before any RPC —
+ including the history scan, so nothing is read on a transfer resume() will not finish."""
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ state = {k: v for k, v in receipt.protocol_state.items() if k != "hookData"}
+ if hook is not None:
+ state["hookData"] = hook
+ b.eth.recover_result = receipt
+ with pytest.raises(NotResumableError, match="hook data"):
+ resume(b, to_progress(plan, receipt.replace(protocol_state=state)), secret_nonce="7scalar")
+ assert b.calls == []
+
+
+def test_resume_refuses_when_the_recovered_allowance_is_gone():
+ """veil guard 2: the approval this checkpoint recorded no longer covers the deposit — something
+ else spent it. Re-approving here would be a second irreversible step resume never owns."""
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ b.eth.recover_result = receipt
+ b.eth.approval_required = True
+ with pytest.raises(NotResumableError, match="allowance"):
+ resume(b, to_progress(plan, receipt), secret_nonce="7scalar")
+ assert "eth.deposit_usdc" not in [c[0] for c in b.calls]
+
+
+def test_resume_of_a_private_mint_without_its_secret_nonce_is_refused_before_any_rpc():
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b, mint_mode="private")
+ b.eth.recover_result = receipt
+ with pytest.raises(ConfigurationError, match="secret_nonce"):
+ resume(b, to_progress(plan, receipt))
+ assert b.calls == []
+ # a public mint has nothing to commit to: the default is fine
+ b2 = FakeBridge()
+ plan2, receipt2 = _xreserve_progress(b2)
+ b2.eth.recover_result = receipt2
+ assert resume(b2, to_progress(plan2, receipt2)).receipt.status is Status.ATTESTATION_PENDING
+ assert b2.calls[1][1]["secret_nonce"] == "0scalar"
+
+
+def test_resume_evm_hyperlane_redispatches_without_re_approving():
+ b = FakeBridge()
+ plan = prepare(b.registry, source="ethereum/wbtc", destination="aleo/wbtc", amount="0.001",
+ recipient=ALEO_RECIPIENT)
+ receipt = Receipt(id=APPROVAL, protocol="hyperlane", status=Status.SOURCE_SUBMISSION_PENDING,
+ protocol_state={"routeId": plan.route_id, "approvalTxIds": [APPROVAL],
+ "sourceSender": EVM_ADDRESS})
+ b.eth.recover_result = receipt
+ out = resume(b, to_progress(plan, receipt))
+ assert [c[0] for c in b.calls] == ["eth.recover_source", "eth.quote_transfer_remote", "eth.transfer_remote"]
+ assert out.receipt.status is Status.SOURCE_CONFIRMING
+ assert out.receipt.protocol_state["approvalTxIds"] == [APPROVAL]
+ b2 = FakeBridge()
+ b2.eth.recover_result = receipt
+ b2.eth.approval_required = True
+ with pytest.raises(NotResumableError, match="allowance"):
+ resume(b2, to_progress(plan, receipt))
+
+
+def test_resume_refuses_a_plan_prepared_for_another_account():
+ b = FakeBridge()
+ plan, receipt = _xreserve_progress(b)
+ stale = prepare(b.registry, source="ethereum/usdc", destination="aleo/usdcx", amount="2",
+ recipient=ALEO_RECIPIENT, sender="0x0000000000000000000000000000000000000009")
+ with pytest.raises(ConfigurationError, match="sender"):
+ resume(b, to_progress(stale, receipt.replace(protocol_state={**receipt.protocol_state,
+ "routeId": stale.route_id})))
+ assert b.calls == []
+
+
+# ── complete ──────────────────────────────────────────────────────────────────
+
+def _ready(b):
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ ready = receipt.replace(status=Status.DESTINATION_ACTION_REQUIRED,
+ next_action={"kind": "xreserve-private-mint", "chainId": "aleo-testnet"},
+ protocol_state={**receipt.protocol_state, "attestation": SIG})
+ return plan, payload, message_hash, ready
+
+
+def test_complete_proves_checkpoints_then_submits_one_private_mint():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, ready = _ready(b)
+ b.xreserve.expected_secret_nonce = "7scalar"
+ cps = []
+ out = complete(b, to_progress(plan, ready), secret_nonce="7scalar", on_checkpoint=cps.append)
+ assert b.calls[-1][0] == "xreserve.private_mint" and b.calls[-1][1]["secret_nonce"] == "7scalar"
+ assert [e[0] for e in b.events] == ["delegate_prepared", "checkpoint:DESTINATION_ACTION_REQUIRED",
+ "submit", "checkpoint:DESTINATION_CONFIRMING"]
+ serialized = json.dumps({"type": "execute", "id": "at1fake1", "fee": {}})
+ assert cps[0].destination == {"preparedTransaction": {"transactionId": "at1fake1",
+ "serializedTransaction": serialized}}
+ assert cps[0].source == {"transactionId": "0x" + "22" * 32}
+ assert cps[1].destination == {"transactionId": "at1fake1"}
+ assert out.next == "wait" and out.receipt.status is Status.DESTINATION_CONFIRMING
+ assert out.receipt.destination_tx_id == "at1fake1" and out.receipt.next_action is None
+ assert out.receipt.protocol_state["payload"] == "0x" + payload.hex()
+ assert out.receipt.protocol_state["destinationFunction"] == "private_mint"
+ assert "preparedDestinationTransaction" not in out.receipt.protocol_state
+ # the nonce, the attestation and the hook never reach the recovery record
+ for cp in cps:
+ text = json.dumps(cp.to_dict())
+ assert "7scalar" not in text and SIG not in text and "attestation" not in text
+ assert "7scalar" not in json.dumps(out.receipt.protocol_state)
+ assert "secretNonce" not in json.dumps(out.receipt.protocol_state)
+
+
+def test_complete_never_writes_a_secret_to_the_bound_checkpoint_store(tmp_path):
+ """Fix round 1 (R2): the same claim as above, but proved against what actually reaches disk —
+ every byte the store wrote, not just the Checkpoint objects handed to the callback. The secret
+ nonce, Circle's attestation and the hook the deposit committed to must appear in none of it."""
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(environment="testnet", checkpoints=store)
+ plan, payload, message_hash, ready = _ready(b)
+ b.xreserve.expected_secret_nonce = "7scalar"
+ written = []
+ real_save = store.save
+ store.save = lambda cp: (written.append(cp.to_json()), real_save(cp))[1]
+
+ complete(b, to_progress(plan, ready), secret_nonce="7scalar")
+
+ hook_hex = payload[-65:].hex() # the commitment the deposit was hooked to
+ secrets = ["7scalar", "secretNonce", SIG, SIG[2:], hook_hex, payload.hex(), "attestation"]
+ on_disk = [p.read_text(encoding="utf-8") for p in tmp_path.glob("*.json")]
+ assert written and on_disk # the store really was exercised
+ for text in written + on_disk:
+ for secret in secrets:
+ assert secret not in text, f"{secret!r} leaked into a checkpoint"
+ # ...and what IS kept is enough to recover the mint
+ assert [c.id for c in store.list()] == [message_hash]
+ assert store.list()[0].destination == {"transactionId": "at1fake1"}
+
+
+def test_complete_never_reaches_proving_with_a_nonce_that_opens_no_commitment():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, ready = _ready(b)
+ b.xreserve.expected_secret_nonce = "7scalar"
+ with pytest.raises(AttestationError):
+ complete(b, to_progress(plan, ready), secret_nonce="8scalar")
+ assert b.events == [] and b.submitted == []
+
+
+def test_complete_of_a_private_mint_without_its_secret_nonce_is_refused_before_any_rpc():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, ready = _ready(b)
+ with pytest.raises(ConfigurationError, match="secret_nonce"):
+ complete(b, to_progress(plan, ready))
+ assert b.calls == [] and b.events == []
+
+
+def test_complete_rebroadcasts_a_prepared_destination_without_reproving():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, ready = _ready(b)
+ serialized = json.dumps({"type": "execute", "id": "at1private", "fee": {}})
+ ready = ready.replace(id="at1private",
+ protocol_state={**ready.protocol_state, "preparedDestinationTransaction": serialized})
+ cps = []
+ out = complete(b, to_progress(plan, ready), on_checkpoint=cps.append) # no secret nonce needed
+ assert b.aleo.submitted == [serialized]
+ assert not any(e[0] == "delegate_prepared" for e in b.events)
+ assert out.receipt.status is Status.DESTINATION_CONFIRMING and out.receipt.destination_tx_id == "at1private"
+ assert "preparedDestinationTransaction" not in out.receipt.protocol_state
+ assert len(cps) == 1 and cps[0].destination == {"transactionId": "at1private"}
+ # the node already knows it: the earlier broadcast won
+ b.aleo.duplicate_on_submit = True
+ assert complete(b, to_progress(plan, ready)).receipt.status is Status.DESTINATION_CONFIRMING
+ # ...but a mismatched id never becomes this transfer's destination transaction
+ b2 = FakeBridge(environment="testnet")
+ b2.aleo.network.submit_transaction = lambda tx: "at1other"
+ with pytest.raises(CheckpointInvalidError, match="expected at1private"):
+ complete(b2, to_progress(plan, ready))
+
+
+def test_complete_guards():
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, ready = _ready(b)
+ with pytest.raises(NotResumableError, match="complete"):
+ complete(b, to_progress(plan, ready.replace(status=Status.ATTESTATION_PENDING, next_action=None)))
+ with pytest.raises(NotResumableError, match="destination action"):
+ complete(b, to_progress(plan, ready.replace(next_action={"kind": "other", "chainId": "aleo-testnet"})))
+ with pytest.raises(AttestationError, match="attestation"):
+ complete(b, to_progress(plan, ready.replace(protocol_state={**ready.protocol_state, "attestation": "zz"})))
+ with pytest.raises(AttestationError):
+ complete(b, to_progress(plan, ready.replace(
+ protocol_state={k: v for k, v in ready.protocol_state.items() if k != "payload"})))
+ with pytest.raises(CheckpointInvalidError, match="does not match the prepared route"):
+ complete(b, to_progress(plan, ready.replace(
+ protocol_state={**ready.protocol_state, "routeId": "other"})))
+ assert b.aleo.submitted == [] and b.submitted == [] and b.events == []
diff --git a/bridge-sdk/tests/test_sealevel_accounts.py b/bridge-sdk/tests/test_sealevel_accounts.py
new file mode 100644
index 0000000..834e4e3
--- /dev/null
+++ b/bridge-sdk/tests/test_sealevel_accounts.py
@@ -0,0 +1,105 @@
+import dataclasses
+
+import pytest
+
+from aleo_bridge import _sealevel as sl
+from aleo_bridge.errors import RouteUnavailableError
+from aleo_bridge.registry import DEFAULT_REGISTRY, Route
+from tests.fakes.sealevel_fixtures import TRANSFER, WARP_PROGRAM_ADDRESS, metadata_from_fixture
+
+ACCOUNTS = TRANSFER["accounts"]
+
+
+def route_with(metadata: dict) -> Route:
+ base = DEFAULT_REGISTRY.route(sl.SOLANA_ROUTE_ID)
+ return dataclasses.replace(base, availability="active", metadata=metadata)
+
+
+def test_pdas_match_the_recorded_transaction():
+ unique = TRANSFER["uniqueMessageAddress"]
+ assert sl.derive_dispatched_message_pda(ACCOUNTS[3]["address"], unique) == ACCOUNTS[8]["address"]
+ assert sl.derive_gas_payment_pda(ACCOUNTS[9]["address"], unique) == ACCOUNTS[11]["address"]
+ dispatched, bump_dispatched = sl.find_program_address(
+ [*sl.DISPATCHED_MESSAGE_SEED_PREFIX, sl.b58decode(unique)], ACCOUNTS[3]["address"])
+ gas_payment, bump_gas = sl.find_program_address(
+ [*sl.GAS_PAYMENT_SEED_PREFIX, sl.b58decode(unique)], ACCOUNTS[9]["address"])
+ assert (dispatched, bump_dispatched) == (ACCOUNTS[8]["address"], 253) # SEALEVEL_NOTES §2 bump seeds
+ assert (gas_payment, bump_gas) == (ACCOUNTS[11]["address"], 255)
+
+
+def test_route_static_pdas_recompute_from_the_warp_program():
+ # The registry's tokenPda / dispatchAuthorityPda / nativeCollateralPda / outbox / IGP program-data are
+ # PDAs of the warp, mailbox and IGP programs (SEALEVEL_NOTES §2-3); bumps 255, 254, 255, 255, 254.
+ assert sl.find_program_address([b"hyperlane_message_recipient", b"-", b"handle", b"-", b"account_metas"], WARP_PROGRAM_ADDRESS) == (ACCOUNTS[2]["address"], 255)
+ assert sl.find_program_address([b"hyperlane_dispatcher", b"-", b"dispatch_authority"], WARP_PROGRAM_ADDRESS) == (ACCOUNTS[5]["address"], 254)
+ assert sl.find_program_address([b"hyperlane_token", b"-", b"native_collateral"], WARP_PROGRAM_ADDRESS) == (ACCOUNTS[15]["address"], 255)
+ assert sl.find_program_address([b"hyperlane", b"-", b"outbox"], ACCOUNTS[3]["address"]) == (ACCOUNTS[4]["address"], 255)
+ assert sl.find_program_address([b"hyperlane_igp", b"-", b"program_data"], ACCOUNTS[9]["address"]) == (ACCOUNTS[10]["address"], 254)
+
+
+def test_pda_derivation_agrees_with_solders_on_random_keys():
+ solders_keypair = pytest.importorskip("solders.keypair")
+ from solders.pubkey import Pubkey
+
+ mailbox = Pubkey.from_string(ACCOUNTS[3]["address"])
+ for _ in range(16):
+ unique = solders_keypair.Keypair().pubkey()
+ seeds = [*sl.DISPATCHED_MESSAGE_SEED_PREFIX, bytes(unique)]
+ expected, expected_bump = Pubkey.find_program_address(seeds, mailbox)
+ assert sl.find_program_address(seeds, str(mailbox)) == (str(expected), expected_bump)
+
+
+def test_find_program_address_rejects_oversized_seeds():
+ with pytest.raises(sl.BridgeError, match="32 bytes"):
+ sl.find_program_address([bytes(33)], WARP_PROGRAM_ADDRESS)
+ with pytest.raises(sl.BridgeError, match="16 seeds"):
+ sl.find_program_address([b"x"] * 17, WARP_PROGRAM_ADDRESS)
+
+
+def test_account_metas_reproduce_the_recorded_16_accounts():
+ metadata = sl.solana_route_metadata(route_with(metadata_from_fixture()))
+ metas = sl.account_metas(metadata, TRANSFER["senderAddress"], TRANSFER["uniqueMessageAddress"])
+ assert len(metas) == 16
+ assert [m.to_dict() for m in metas] == ACCOUNTS
+
+
+def test_account_metas_omit_the_optional_overhead_slot():
+ metadata = sl.solana_route_metadata(route_with(metadata_from_fixture(overhead=False)))
+ metas = sl.account_metas(metadata, TRANSFER["senderAddress"], TRANSFER["uniqueMessageAddress"])
+ overhead = ACCOUNTS[12]["address"]
+ assert len(metas) == 15
+ assert overhead not in [m.address for m in metas]
+ assert [m.to_dict() for m in metas] == [a for a in ACCOUNTS if a["address"] != overhead]
+
+
+def test_default_registry_route_carries_the_recorded_deployment():
+ live = sl.solana_route_metadata(DEFAULT_REGISTRY.route(sl.SOLANA_ROUTE_ID))
+ recorded = sl.solana_route_metadata(route_with(metadata_from_fixture()))
+ for field in ("warp_program_address", "token_pda", "native_collateral_pda", "dispatch_authority_pda",
+ "mailbox_program_address", "mailbox_outbox_pda", "igp_program_address", "igp_program_data_pda",
+ "igp_account", "igp_overhead_account", "spl_noop_program_address", "destination_domain",
+ "destination_gas_amount", "registry_commit"):
+ assert getattr(live, field) == getattr(recorded, field), field
+ assert live.destination_gas_amount == 464_000
+ assert live.igp_overhead_account == "AkeHBbE5JkwVppujCQQ6WuxsVsJtruBAjUo6fDCFp6fF"
+
+
+def test_route_metadata_validation():
+ inactive = dataclasses.replace(DEFAULT_REGISTRY.route(sl.SOLANA_ROUTE_ID), availability="metadata-required")
+ with pytest.raises(RouteUnavailableError, match="not executable"):
+ sl.solana_route_metadata(inactive)
+ with pytest.raises(RouteUnavailableError, match="invalid igpAccount"):
+ sl.solana_route_metadata(route_with({**metadata_from_fixture(), "igpAccount": "not-a-solana-address"}))
+ with pytest.raises(RouteUnavailableError, match="invalid destinationDomain"):
+ sl.solana_route_metadata(route_with({**metadata_from_fixture(), "destinationDomain": "1634493807"}))
+ with pytest.raises(RouteUnavailableError, match="invalid destinationGasAmount"):
+ sl.solana_route_metadata(route_with({**metadata_from_fixture(), "destinationGasAmount": "lots"}))
+ with pytest.raises(RouteUnavailableError, match="invalid registryCommit"):
+ sl.solana_route_metadata(route_with({**metadata_from_fixture(), "registryCommit": "418056e2"}))
+ missing = metadata_from_fixture()
+ del missing["mailboxOutboxPda"]
+ with pytest.raises(RouteUnavailableError, match="invalid mailboxOutboxPda"):
+ sl.solana_route_metadata(route_with(missing))
+ xreserve = DEFAULT_REGISTRY.route("xreserve:ethereum/usdc->aleo/usdcx")
+ with pytest.raises(RouteUnavailableError, match="Hyperlane"):
+ sl.solana_route_metadata(xreserve)
diff --git a/bridge-sdk/tests/test_sealevel_igp.py b/bridge-sdk/tests/test_sealevel_igp.py
new file mode 100644
index 0000000..ce08dfc
--- /dev/null
+++ b/bridge-sdk/tests/test_sealevel_igp.py
@@ -0,0 +1,71 @@
+import pytest
+
+from aleo_bridge import _sealevel as sl
+from aleo_bridge.errors import BridgeError
+from tests.fakes.sealevel_fixtures import (
+ ALEO_MAINNET_DOMAIN,
+ DESTINATION_GAS_AMOUNT,
+ EXPECTED_IGP_PAYMENT_LAMPORTS,
+ IGP,
+ igp_account_data,
+)
+
+HEADER_NO_OWNER = 1 + 8 + 1 + 32 + 1 + 32 + 4 # initialized, disc, bump, salt, owner=None, beneficiary, count
+
+
+def synthetic_igp(domain: int, *, tag: int = 0, rate: int = 1, price: int = 1, decimals: int = 9) -> bytes:
+ data = bytearray(HEADER_NO_OWNER + sl.GAS_ORACLE_ENTRY_BYTES)
+ data[0] = 1
+ data[1:9] = sl.IGP_DISCRIMINATOR
+ data[HEADER_NO_OWNER - 4:HEADER_NO_OWNER] = (1).to_bytes(4, "little")
+ entry = HEADER_NO_OWNER
+ data[entry:entry + 4] = domain.to_bytes(4, "little")
+ data[entry + 4] = tag
+ data[entry + 5:entry + 21] = rate.to_bytes(16, "little")
+ data[entry + 21:entry + 37] = price.to_bytes(16, "little")
+ data[entry + 37] = decimals
+ return bytes(data)
+
+
+def test_decode_recorded_inner_igp_account():
+ account = sl.decode_igp_account(igp_account_data())
+ assert account.bump == 255
+ assert account.owner is not None and len(account.owner) in (43, 44)
+ assert len(account.beneficiary) in (43, 44)
+ assert account.gas_oracles[ALEO_MAINNET_DOMAIN] == sl.GasOracle(751_705_303_136, 83_169, 6)
+ assert len(account.gas_oracles) + len(account.unsupported_oracles) == 42
+
+
+def test_quote_reproduces_sealevel_notes_vector():
+ assert sl.quote_igp_lamports(igp_account_data(), ALEO_MAINNET_DOMAIN, DESTINATION_GAS_AMOUNT) == EXPECTED_IGP_PAYMENT_LAMPORTS
+ assert sl.igp_lamports(sl.GasOracle(751_705_303_136, 83_169, 6), 464_000) == 2_900_000
+
+
+def test_igp_lamports_divides_when_token_decimals_exceed_nine():
+ # dest_cost = 10^6 * 1 ; origin_cost = 10^6 * 10^19 / 10^19 = 10^6 ; decimals 12 → // 10^3
+ assert sl.igp_lamports(sl.GasOracle(10 ** 19, 1, 12), 10 ** 6) == 1_000
+ assert sl.igp_lamports(sl.GasOracle(10 ** 19, 1, 9), 10 ** 6) == 1_000_000
+
+
+def test_quote_rejects_missing_domain_and_unknown_variant_tag():
+ with pytest.raises(BridgeError, match="no gas-oracle entry for destination domain 999999999"):
+ sl.quote_igp_lamports(igp_account_data(), 999_999_999, 1)
+ with pytest.raises(BridgeError, match="unexpected GasOracle variant tag 7"):
+ sl.quote_igp_lamports(synthetic_igp(42, tag=7), 42, 1)
+ assert sl.quote_igp_lamports(synthetic_igp(42, rate=10 ** 19, price=5, decimals=9), 42, 3) == 15
+
+
+def test_decode_rejects_malformed_layouts():
+ with pytest.raises(BridgeError, match="not initialized"):
+ sl.decode_igp_account(bytes(12))
+ with pytest.raises(BridgeError, match="declared layout exceeds the supplied bytes"):
+ sl.decode_igp_account(b"\x01" + sl.IGP_DISCRIMINATOR + bytes(3))
+ with pytest.raises(BridgeError, match="discriminator"):
+ sl.decode_igp_account(b"\x01" + b"WRONGDIS" + bytes(80))
+ bad_owner = bytearray(80)
+ bad_owner[0] = 1
+ bad_owner[1:9] = sl.IGP_DISCRIMINATOR
+ bad_owner[42] = 2
+ with pytest.raises(BridgeError, match="owner option tag 2"):
+ sl.decode_igp_account(bytes(bad_owner))
+ assert IGP["address"] == "JAvHW21tYXE9dtdG83DReqU2b4LUexFuCbtJT5tF8X6M"
diff --git a/bridge-sdk/tests/test_sealevel_instruction.py b/bridge-sdk/tests/test_sealevel_instruction.py
new file mode 100644
index 0000000..f7542ff
--- /dev/null
+++ b/bridge-sdk/tests/test_sealevel_instruction.py
@@ -0,0 +1,41 @@
+import base64
+
+import pytest
+
+from aleo_bridge import _sealevel as sl
+from aleo_bridge.encoding import aleo_address_to_bytes32
+from aleo_bridge.errors import BridgeError
+from tests.fakes.sealevel_fixtures import ALEO_MAINNET_DOMAIN, TRANSFER
+
+RECIPIENT32_HEX = "1c3496991e7c611ced5ee5cd0cdee969c53efc8a5497ae050819b1ef00ed2912"
+
+
+def test_instruction_data_matches_mainnet_fixture_byte_for_byte():
+ data = sl.build_transfer_remote_instruction_data(
+ ALEO_MAINNET_DOMAIN,
+ aleo_address_to_bytes32(TRANSFER["recipientAleoAddress"]),
+ TRANSFER["amountLamports"],
+ )
+ assert len(data) == sl.INSTRUCTION_DATA_BYTES == 77
+ assert base64.b64encode(data).decode() == TRANSFER["instructionDataBase64"]
+
+
+def test_instruction_data_layout_offsets():
+ data = sl.build_transfer_remote_instruction_data(ALEO_MAINNET_DOMAIN, bytes.fromhex(RECIPIENT32_HEX), 676_200_000_000)
+ assert data[0:8] == bytes([1] * 8) == sl.PROGRAM_INSTRUCTION_DISCRIMINATOR
+ assert data[8] == sl.TRANSFER_REMOTE_VARIANT_TAG == 1
+ assert data[9:13] == bytes.fromhex("6f656c61") # 0x616c656f little-endian
+ assert data[13:45] == bytes.fromhex(RECIPIENT32_HEX) # raw bech32m payload, no reversal
+ assert data[45:77] == (676_200_000_000).to_bytes(32, "little")
+ assert data[45:49] == bytes.fromhex("002aa970")
+
+
+def test_instruction_data_rejects_bad_inputs():
+ with pytest.raises(BridgeError, match="32 bytes"):
+ sl.build_transfer_remote_instruction_data(ALEO_MAINNET_DOMAIN, bytes(31), 1)
+ with pytest.raises(BridgeError, match="32-byte unsigned"):
+ sl.build_transfer_remote_instruction_data(ALEO_MAINNET_DOMAIN, bytes(32), 1 << 256)
+ with pytest.raises(BridgeError, match="32-byte unsigned"):
+ sl.build_transfer_remote_instruction_data(ALEO_MAINNET_DOMAIN, bytes(32), -1)
+ with pytest.raises(BridgeError, match="destination domain"):
+ sl.build_transfer_remote_instruction_data(1 << 32, bytes(32), 1)
diff --git a/bridge-sdk/tests/test_sol_bridge.py b/bridge-sdk/tests/test_sol_bridge.py
new file mode 100644
index 0000000..38c9ac6
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_bridge.py
@@ -0,0 +1,119 @@
+"""Task 8: wiring the Solana connection into Bridge (sol property, from_env, status)."""
+import pytest
+
+pytest.importorskip("solders")
+from solders.keypair import Keypair
+
+import aleo_bridge
+from aleo_bridge import Bridge, Solana
+from aleo_bridge._base58 import b58encode
+from aleo_bridge.errors import ConfigurationError
+from aleo_bridge.sol import SolModule
+from aleo_bridge.types import ChainStatus
+from tests.conftest import FakeAleo
+from tests.fakes.fake_solana import FakeSolanaClient
+
+
+def test_sol_property_raises_when_solana_is_not_configured():
+ bridge = Bridge(FakeAleo())
+ assert bridge.solana is None
+ with pytest.raises(ConfigurationError, match="Solana is not configured"):
+ bridge.sol
+
+
+def test_bare_client_is_wrapped_read_only():
+ fake = FakeSolanaClient(balance=5)
+ bridge = Bridge(FakeAleo(), solana=fake)
+ assert isinstance(bridge.solana, Solana) and bridge.solana.client is fake
+ assert bridge.solana.can_sign is False and bridge.solana.address is None
+ assert isinstance(bridge.sol, SolModule) and bridge.sol is bridge.sol
+ with pytest.raises(ConfigurationError, match="read-only"):
+ bridge.sol.balance()
+ solana_status = [c for c in bridge.status().chains if c.chain_id == "solana"]
+ assert solana_status == [ChainStatus(chain_id="solana", address=None, can_sign=False, balances={})]
+
+
+def test_configured_connection_reports_sol_balance_in_status():
+ keypair = Keypair()
+ fake = FakeSolanaClient(balance=1_234)
+ bridge = Bridge(FakeAleo(), solana=Solana(client=fake, signer=keypair))
+ assert bridge.solana.address == str(keypair.pubkey())
+ solana_status = [c for c in bridge.status().chains if c.chain_id == "solana"][0]
+ assert solana_status.address == str(keypair.pubkey()) and solana_status.can_sign is True
+ assert solana_status.balances == {"solana/sol": 1_234}
+
+
+def test_an_rpc_url_string_becomes_a_read_only_connection():
+ bridge = Bridge(FakeAleo(), solana="https://rpc.example")
+ assert isinstance(bridge.solana, Solana) and bridge.solana.rpc_url == "https://rpc.example"
+ assert bridge.solana.can_sign is False
+
+
+def test_an_object_that_is_not_a_solana_client_is_refused():
+ class NotAClient:
+ def get_latest_blockhash(self, commitment=None): # pragma: no cover - never called
+ return None
+
+ with pytest.raises(ConfigurationError, match="solana="):
+ Bridge(FakeAleo(), solana=NotAClient()) # no get_account_info
+ with pytest.raises(ConfigurationError, match="solana="):
+ Bridge(FakeAleo(), solana=object())
+
+
+def test_status_omits_the_solana_row_when_the_environment_has_no_solana_chain():
+ """Only mainnet has a Solana chain; a testnet client must not invent a "solana" row."""
+ bridge = Bridge(FakeAleo(network_name="testnet"), solana=FakeSolanaClient())
+ assert [c.chain_id for c in bridge.status().chains if c.chain_id == "solana"] == []
+
+
+def test_status_derives_the_solana_chain_and_native_asset_from_the_registry():
+ keypair = Keypair()
+ bridge = Bridge(FakeAleo(), solana=Solana(client=FakeSolanaClient(balance=7), signer=keypair))
+ chain = [c for c in bridge.registry.chains(environment="mainnet") if c.family == "solana"][0]
+ native = [a for a in bridge.registry.assets(chain=chain.id) if a.kind == "native"][0]
+ row = [c for c in bridge.status().chains if c.chain_id == chain.id][0]
+ assert row.balances == {native.id: 7}
+
+
+def test_from_env_builds_the_solana_connection(monkeypatch):
+ key = b58encode(bytes(Keypair()))
+ seen = {}
+
+ def capture_init(self, aleo, *, ethereum=None, solana=None, environment=None, registry=None, checkpoints=None):
+ seen["solana"] = solana
+
+ monkeypatch.setattr(Bridge, "__init__", capture_init)
+ monkeypatch.setattr("aleo_bridge.client.build_aleo", lambda *args, **kwargs: FakeAleo())
+ for var in ("EVM_PRIVATE_KEY", "ETHEREUM_RPC_URL", "BRIDGE_EVM_PRIVATE_KEY", "BRIDGE_LIVE_ETHEREUM_RPC_URL",
+ "BRIDGE_SOLANA_PRIVATE_KEY", "BRIDGE_LIVE_SOLANA_RPC_URL", "BRIDGE_CHECKPOINT_DIR"):
+ monkeypatch.delenv(var, raising=False)
+ monkeypatch.setenv("BRIDGE_PRIVATE_KEY", "APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH")
+ monkeypatch.setenv("SOLANA_PRIVATE_KEY", key)
+ monkeypatch.setenv("SOLANA_RPC_URL", "https://rpc.example")
+ Bridge.from_env()
+ assert isinstance(seen["solana"], Solana) and seen["solana"].can_sign and seen["solana"].rpc_url == "https://rpc.example"
+ override = Solana(client=FakeSolanaClient())
+ Bridge.from_env(solana=override)
+ assert seen["solana"] is override
+
+
+def test_package_exports():
+ assert aleo_bridge.Solana is Solana
+ assert aleo_bridge.SolModule is SolModule
+ assert aleo_bridge.DEFAULT_SOLANA_RPC_URL == "https://api.mainnet-beta.solana.com"
+ from aleo_bridge._calls import AleoCall, EvmCall, SolCall
+ from aleo_bridge.eth import EthModule
+ from aleo_bridge.freezelist import FreezeList
+ from aleo_bridge.hyperlane import HyperlaneModule
+ from aleo_bridge.profile import Profile
+ from aleo_bridge.xreserve import XReserveModule
+
+ assert aleo_bridge.SolCall is SolCall
+ # pre-existing exports (plans 1/2/4) must still be exported after this task's __init__.py edit
+ assert aleo_bridge.EthModule is EthModule
+ assert aleo_bridge.HyperlaneModule is HyperlaneModule
+ assert aleo_bridge.XReserveModule is XReserveModule
+ assert aleo_bridge.AleoCall is AleoCall
+ assert aleo_bridge.EvmCall is EvmCall
+ assert aleo_bridge.FreezeList is FreezeList
+ assert aleo_bridge.Profile is Profile
diff --git a/bridge-sdk/tests/test_sol_connection.py b/bridge-sdk/tests/test_sol_connection.py
new file mode 100644
index 0000000..ae503ff
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_connection.py
@@ -0,0 +1,325 @@
+import importlib
+import json
+import sys
+
+import pytest
+
+from aleo_bridge import sol
+from aleo_bridge._base58 import b58encode
+from aleo_bridge.errors import ConfigurationError, MissingExtraError
+
+
+class _Reader:
+ """Stands in for a caller-configured solana-py Client; never called in this file."""
+
+
+def _block_solders_and_solana(monkeypatch):
+ """Block ``solders``/``solana`` for the duration of the test, reverted automatically by
+ ``monkeypatch``. ``_libs()`` imports submodules (``from solders.hash import Hash``, ...), and
+ once any earlier test in the run has imported those submodules for real, Python's import
+ machinery resolves them straight from ``sys.modules`` without re-checking the (now ``None``)
+ top-level package — so a plain ``sys.modules["solders"] = None`` only blocks a *first* import.
+ Purging every already-cached ``solders``/``solana`` submodule first makes the block work
+ regardless of test order."""
+ for name in list(sys.modules):
+ if name == "solders" or name.startswith("solders.") or name == "solana" or name.startswith("solana."):
+ monkeypatch.delitem(sys.modules, name, raising=False)
+ monkeypatch.setitem(sys.modules, "solders", None)
+ monkeypatch.setitem(sys.modules, "solana", None)
+
+
+def test_package_imports_without_solders_or_solana(monkeypatch):
+ """``import aleo_bridge`` and ``from aleo_bridge import Solana`` must work with solders/solana
+ absent; only the first call that actually needs them raises MissingExtraError. Mirrors
+ test_import_without_web3.py's monkeypatch-and-revert pattern so the blocked modules and the
+ reimported aleo_bridge never leak into tests that run after this one."""
+ _block_solders_and_solana(monkeypatch)
+ for name in list(sys.modules):
+ if name.startswith("aleo_bridge"):
+ monkeypatch.delitem(sys.modules, name)
+
+ pkg = importlib.import_module("aleo_bridge")
+ assert pkg.Solana is not None
+
+ from aleo_bridge.errors import MissingExtraError as ReimportedMissingExtraError
+
+ with pytest.raises(ReimportedMissingExtraError) as exc_info:
+ pkg.Solana()
+ assert "aleo-bridge-sdk[solana]" in str(exc_info.value)
+
+
+def test_read_only_connection_needs_no_solana_extra(monkeypatch):
+ monkeypatch.setattr(sol, "_LIBS", None)
+ _block_solders_and_solana(monkeypatch)
+ conn = sol.Solana(client=_Reader())
+ assert conn.address is None and conn.can_sign is False and conn.rpc_url is None
+ with pytest.raises(ConfigurationError, match="read-only"):
+ conn.sign_message(b"payload")
+ with pytest.raises(ConfigurationError, match="read-only"):
+ conn.pubkey
+ with pytest.raises(MissingExtraError, match=r"aleo-bridge-sdk\[solana\]"):
+ sol.Solana()
+ with pytest.raises(MissingExtraError):
+ sol.Solana(client=_Reader(), private_key="[1,2,3]")
+
+
+def test_constructor_argument_conflicts():
+ with pytest.raises(ConfigurationError, match="rpc_url or client"):
+ sol.Solana("https://rpc.example", client=_Reader())
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ with pytest.raises(ConfigurationError, match="signer or private_key"):
+ sol.Solana(client=_Reader(), signer=Keypair(), private_key=b58encode(bytes(Keypair())))
+
+
+def test_default_transport_is_mainnet_beta_at_confirmed_commitment():
+ pytest.importorskip("solders")
+
+ conn = sol.Solana()
+ assert sol.DEFAULT_SOLANA_RPC_URL == "https://api.mainnet-beta.solana.com"
+ assert conn.rpc_url == sol.DEFAULT_SOLANA_RPC_URL
+ assert isinstance(conn.client, sol.SolanaRpcClient)
+ assert conn.client.url == sol.DEFAULT_SOLANA_RPC_URL and conn.client.commitment == "confirmed"
+ custom = sol.Solana("https://rpc.example")
+ assert custom.rpc_url == "https://rpc.example" and custom.client.url == "https://rpc.example"
+
+
+def test_async_solana_py_client_is_adapted_onto_a_private_loop():
+ pytest.importorskip("solders")
+ from solders.hash import Hash
+
+ class FakeProvider:
+ async def make_request(self, request, response_type):
+ return sol.RpcResult(True)
+
+ class FakeAsyncClient: # the shape of solana-py ≥ 0.36 AsyncClient
+ _provider = FakeProvider()
+
+ def __init__(self):
+ self.sent = []
+
+ async def get_balance(self, pubkey, commitment=None):
+ return sol.RpcResult(7)
+
+ async def send_raw_transaction(self, txn, opts=None):
+ self.sent.append((bytes(txn), opts))
+ return sol.RpcResult("sig")
+
+ fake = FakeAsyncClient()
+ conn = sol.Solana(client=fake)
+ assert isinstance(conn.client, sol._AsyncClientAdapter) and conn.can_sign is False
+ assert conn.client.get_balance(None).value == 7 # coroutine run synchronously
+ assert conn.client.is_blockhash_valid(Hash.default()).value is True # supplied by the adapter
+ pytest.importorskip("solana")
+ assert conn.client.send_raw_transaction(b"\x01").value == "sig"
+ sent_tx, tx_opts = fake.sent[0]
+ assert sent_tx == b"\x01" and tx_opts.skip_preflight is False and tx_opts.skip_confirmation is True
+ assert str(tx_opts.preflight_commitment) == "confirmed"
+
+
+def test_private_key_forms():
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ keypair = Keypair()
+ secret = bytes(keypair) # 64 bytes: seed || pubkey
+ assert len(secret) == 64
+ for private_key in (b58encode(secret), json.dumps(list(secret)), " " + json.dumps(list(secret)) + "\n", secret):
+ conn = sol.Solana(client=_Reader(), private_key=private_key)
+ assert conn.address == str(keypair.pubkey()) and conn.can_sign is True
+ assert conn.pubkey == keypair.pubkey()
+ assert sol.keypair_from_private_key(secret[:32]).pubkey() == keypair.pubkey() # 32-byte seed
+ for bad in ("not-base58-0OIl", "[1, 2, 3]", "[1, 2, \"x\"]", "[", bytes(63)):
+ with pytest.raises(ConfigurationError):
+ sol.keypair_from_private_key(bad)
+
+
+def test_keypair_signer_signs_the_message():
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ keypair = Keypair()
+ conn = sol.Solana(client=_Reader(), signer=keypair)
+ assert conn.address == str(keypair.pubkey())
+ assert conn.sign_message(b"payload") == keypair.sign_message(b"payload")
+
+
+def test_any_object_with_pubkey_and_sign_message_is_a_signer():
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ inner = Keypair()
+
+ class RemoteSigner: # e.g. HSM / remote signing service
+ def pubkey(self):
+ return inner.pubkey()
+
+ def sign_message(self, message: bytes):
+ return inner.sign_message(message)
+
+ conn = sol.Solana(client=_Reader(), signer=RemoteSigner())
+ assert isinstance(RemoteSigner(), sol.SolanaSigner)
+ assert conn.can_sign and conn.address == str(inner.pubkey())
+ assert conn.sign_message(b"m") == inner.sign_message(b"m")
+ with pytest.raises(ConfigurationError, match="pubkey\\(\\) and sign_message"):
+ sol.Solana(client=_Reader(), signer=object())
+
+
+def test_from_env():
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ key = b58encode(bytes(Keypair()))
+ assert sol.Solana.from_env({}) is None
+ with_key = sol.Solana.from_env({"SOLANA_PRIVATE_KEY": key})
+ assert with_key is not None and with_key.can_sign and with_key.rpc_url == sol.DEFAULT_SOLANA_RPC_URL
+ with_url = sol.Solana.from_env({"SOLANA_PRIVATE_KEY": key, "SOLANA_RPC_URL": "https://rpc.example"})
+ assert with_url is not None and with_url.rpc_url == "https://rpc.example"
+ read_only = sol.Solana.from_env({"SOLANA_RPC_URL": "https://rpc.example"})
+ assert read_only is not None and read_only.can_sign is False
+
+
+def test_async_client_adapter_close_stops_the_thread():
+ pytest.importorskip("solders")
+
+ class FakeAsyncClient:
+ async def get_balance(self, pubkey, commitment=None):
+ return sol.RpcResult(7)
+
+ adapter = sol._AsyncClientAdapter(FakeAsyncClient())
+ assert adapter._thread.is_alive()
+ adapter.close()
+ assert adapter._thread.is_alive() is False
+ adapter.close() # idempotent: no error, no hang
+
+
+def test_solana_context_manager_closes_the_wrapped_adapter():
+ pytest.importorskip("solders")
+
+ class FakeAsyncClient:
+ async def get_balance(self, pubkey, commitment=None):
+ return sol.RpcResult(7)
+
+ fake = FakeAsyncClient()
+ with sol.Solana(client=fake) as conn:
+ adapter = conn.client
+ assert isinstance(adapter, sol._AsyncClientAdapter)
+ assert adapter._thread.is_alive()
+ assert adapter._thread.is_alive() is False
+
+
+def test_adapter_close_awaits_the_wrapped_clients_own_close_on_its_loop():
+ """solana-py's AsyncClient owns an aiohttp session that can only be closed from its loop;
+ stopping the thread first would leak it (and warn)."""
+ pytest.importorskip("solders")
+
+ class FakeAsyncClient:
+ def __init__(self):
+ self.closed_on = None
+
+ async def get_balance(self, pubkey, commitment=None):
+ return sol.RpcResult(7)
+
+ async def close(self):
+ import threading as _threading
+ self.closed_on = _threading.current_thread().name
+
+ fake = FakeAsyncClient()
+ adapter = sol._AsyncClientAdapter(fake)
+ loop_thread = adapter._thread.name
+ adapter.close()
+ assert fake.closed_on == loop_thread # awaited on the private loop, before it stopped
+ assert adapter._thread.is_alive() is False
+ adapter.close() # idempotent: closes the client exactly once
+
+
+def test_adapter_close_still_stops_the_thread_when_the_wrapped_client_close_raises():
+ """A third-party client that fails to close must not leak our private event-loop thread."""
+ pytest.importorskip("solders")
+
+ class ExplodingClient:
+ def __init__(self):
+ self.attempts = 0
+
+ async def get_balance(self, pubkey, commitment=None): # pragma: no cover - never called
+ return sol.RpcResult(7)
+
+ def close(self):
+ self.attempts += 1
+ raise OSError("socket already gone")
+
+ fake = ExplodingClient()
+ adapter = sol._AsyncClientAdapter(fake)
+ adapter.close()
+ assert adapter._thread.is_alive() is False
+ adapter.close() # idempotent: no second close attempt
+ assert fake.attempts == 1
+
+
+def test_adapter_close_still_stops_the_thread_when_an_async_client_close_raises():
+ pytest.importorskip("solders")
+
+ class ExplodingAsyncClient:
+ async def get_balance(self, pubkey, commitment=None): # pragma: no cover - never called
+ return sol.RpcResult(7)
+
+ async def close(self):
+ raise OSError("session already detached")
+
+ adapter = sol._AsyncClientAdapter(ExplodingAsyncClient())
+ adapter.close()
+ assert adapter._thread.is_alive() is False
+
+
+def test_solana_exit_never_raises_even_when_the_client_close_fails():
+ pytest.importorskip("solders")
+
+ class ExplodingClient:
+ def get_latest_blockhash(self, commitment=None): # pragma: no cover - never called
+ return None
+
+ def close(self):
+ raise OSError("socket already gone")
+
+ with sol.Solana(client=ExplodingClient()) as conn:
+ assert conn.client is not None
+ with pytest.raises(OSError): # an explicit close() still reports it
+ sol.Solana(client=ExplodingClient()).close()
+
+
+def test_rpc_client_close_closes_its_session():
+ pytest.importorskip("solders")
+
+ class FakeSession:
+ def __init__(self):
+ self.closed = 0
+
+ def close(self):
+ self.closed += 1
+
+ session = FakeSession()
+ conn = sol.Solana(client=sol.SolanaRpcClient("https://rpc.example", session=session))
+ conn.close()
+ assert session.closed == 1 # the default transport releases its pool
+
+
+def test_close_is_a_noop_for_a_connection_without_a_closeable_client():
+ conn = sol.Solana(client=_Reader())
+ conn.close() # no close() on the client — must not raise
+ conn.close()
+
+
+def test_from_env_aliases():
+ pytest.importorskip("solders")
+ from solders.keypair import Keypair
+
+ key = b58encode(bytes(Keypair()))
+ aliased = sol.Solana.from_env({"BRIDGE_SOLANA_PRIVATE_KEY": key})
+ assert aliased is not None and aliased.can_sign and aliased.rpc_url == sol.DEFAULT_SOLANA_RPC_URL
+ aliased_url = sol.Solana.from_env({"BRIDGE_SOLANA_PRIVATE_KEY": key, "BRIDGE_LIVE_SOLANA_RPC_URL": "https://rpc.example"})
+ assert aliased_url is not None and aliased_url.rpc_url == "https://rpc.example"
+ primary_wins = sol.Solana.from_env({"SOLANA_PRIVATE_KEY": key, "BRIDGE_SOLANA_PRIVATE_KEY": "ignored",
+ "SOLANA_RPC_URL": "https://primary.example",
+ "BRIDGE_LIVE_SOLANA_RPC_URL": "https://alias.example"})
+ assert primary_wins is not None and primary_wins.rpc_url == "https://primary.example"
diff --git a/bridge-sdk/tests/test_sol_quote.py b/bridge-sdk/tests/test_sol_quote.py
new file mode 100644
index 0000000..4f65838
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_quote.py
@@ -0,0 +1,165 @@
+import dataclasses
+
+import pytest
+
+pytest.importorskip("solders")
+from solders.keypair import Keypair
+
+from aleo_bridge import _sealevel as sl
+from aleo_bridge.errors import (
+ BridgeError,
+ ConfigurationError,
+ InvalidAmountError,
+ InvalidRecipientError,
+ RegistryVersionMismatchError,
+ RouteNotFoundError,
+)
+from aleo_bridge.sol import SolModule, Solana
+from aleo_bridge.types import SolanaHyperlaneQuote
+from tests.fakes.fake_solana import FakeSolanaClient, stub_bridge
+from tests.fakes.sealevel_fixtures import (
+ DISPATCHED_MESSAGE_RENT_LAMPORTS,
+ EXPECTED_IGP_PAYMENT_LAMPORTS,
+ FEE_PAYER_RENT_LAMPORTS,
+ GAS_PAYMENT_RENT_LAMPORTS,
+ NETWORK_FEE_LAMPORTS,
+ TRANSFER,
+ WARP_PROGRAM_ADDRESS,
+)
+
+RECIPIENT = TRANSFER["recipientAleoAddress"]
+SENDER = TRANSFER["senderAddress"]
+AMOUNT = TRANSFER["amountLamports"]
+RENT = GAS_PAYMENT_RENT_LAMPORTS + DISPATCHED_MESSAGE_RENT_LAMPORTS + FEE_PAYER_RENT_LAMPORTS
+
+
+def module(fake=None, *, signer=None, environment="mainnet") -> tuple[SolModule, FakeSolanaClient]:
+ fake = fake or FakeSolanaClient()
+ return SolModule(stub_bridge(environment), Solana(client=fake, signer=signer)), fake
+
+
+def test_quote_with_pinned_sender_on_read_only_connection():
+ mod, fake = module()
+ quote = mod.quote_transfer_remote(RECIPIENT, amount="676.2", sender=SENDER)
+ assert isinstance(quote, SolanaHyperlaneQuote) and quote.kind == "solana-hyperlane"
+ assert quote.igp_lamports == EXPECTED_IGP_PAYMENT_LAMPORTS
+ assert quote.network_fee_lamports == NETWORK_FEE_LAMPORTS
+ assert quote.rent_lamports == RENT == 5_004_240
+ assert quote.total_lamports == AMOUNT + EXPECTED_IGP_PAYMENT_LAMPORTS + NETWORK_FEE_LAMPORTS + RENT == 676_207_914_240
+ assert quote.plan.route_id == sl.SOLANA_ROUTE_ID
+ assert quote.plan.sender == SENDER and quote.plan.recipient == RECIPIENT
+ assert quote.plan.amount == "676.2" and quote.plan.amount_atomic == AMOUNT
+ assert quote.plan.mint_mode == "public" and [s.id for s in quote.plan.steps] == ["source-dispatch", "message-delivery", "destination-confirmation"]
+ assert quote.amount_out == "676.2"
+ assert sl.SOLANA_PUBKEY_RE.match(quote.unique_message_address)
+ assert [(f.kind, f.amount) for f in quote.fees] == [("interchain-gas", "0.0029"), ("network", "0.00001"), ("rent", "0.00500424")]
+ assert "send_raw_transaction" not in fake.calls and "get_balance" not in fake.calls
+
+
+def test_quote_fee_message_is_a_v0_message_with_compute_budget_and_transfer_remote():
+ mod, fake = module(signer=Keypair())
+ quote = mod.quote_transfer_remote(RECIPIENT, amount_atomic=1)
+ assert quote.plan.sender == mod.conn.address
+ message = fake.fee_messages[0]
+ assert message.header.num_required_signatures == 2
+ assert message.header.num_readonly_signed_accounts == 1
+ programs = [str(message.account_keys[ix.program_id_index]) for ix in message.instructions]
+ assert programs == ["ComputeBudget111111111111111111111111111111", WARP_PROGRAM_ADDRESS]
+ assert bytes(message.instructions[0].data) == bytes.fromhex("02801a0600") # SetComputeUnitLimit(400_000)
+ assert len(bytes(message.instructions[1].data)) == 77
+ assert str(message.recent_blockhash) == WARP_PROGRAM_ADDRESS
+ assert fake.calls.count("get_minimum_balance_for_rent_exemption") == 3
+
+
+def test_quote_uses_amount_atomic_and_rejects_ambiguous_amounts():
+ mod, _ = module()
+ assert mod.quote_transfer_remote(RECIPIENT, amount_atomic=1, sender=SENDER).plan.amount == "0.000000001"
+ with pytest.raises(InvalidAmountError):
+ mod.quote_transfer_remote(RECIPIENT, sender=SENDER)
+ with pytest.raises(InvalidAmountError):
+ mod.quote_transfer_remote(RECIPIENT, amount="1", amount_atomic=1, sender=SENDER)
+ with pytest.raises(InvalidAmountError):
+ mod.quote_transfer_remote(RECIPIENT, amount_atomic=0, sender=SENDER)
+
+
+def test_quote_error_paths():
+ mod, _ = module()
+ with pytest.raises(ConfigurationError, match="sender"):
+ mod.quote_transfer_remote(RECIPIENT, amount_atomic=1)
+ with pytest.raises(InvalidRecipientError):
+ mod.quote_transfer_remote("aleo1notanaddress", amount_atomic=1, sender=SENDER)
+ missing_igp, _ = module(FakeSolanaClient(accounts={}))
+ with pytest.raises(BridgeError, match="IGP account does not exist"):
+ missing_igp.quote_transfer_remote(RECIPIENT, amount_atomic=1, sender=SENDER)
+ testnet, _ = module(environment="testnet")
+ with pytest.raises(RouteNotFoundError):
+ testnet.quote_transfer_remote(RECIPIENT, amount_atomic=1, sender=SENDER)
+
+
+def test_quote_with_plan_checks_registry_version_and_reuses_the_plan():
+ mod, _ = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=5, sender=SENDER).plan
+ quoted = mod.quote_transfer_remote(RECIPIENT, plan=plan)
+ assert quoted.plan is plan and quoted.total_lamports == 5 + EXPECTED_IGP_PAYMENT_LAMPORTS + NETWORK_FEE_LAMPORTS + RENT
+ stale = dataclasses.replace(plan, registry_version="2020-01-01.stale.0")
+ with pytest.raises(RegistryVersionMismatchError):
+ mod.quote_transfer_remote(RECIPIENT, plan=stale)
+
+
+def test_a_plan_alone_supplies_recipient_amount_and_sender():
+ mod, _ = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=5, sender=SENDER).plan
+ quoted = mod.quote_transfer_remote(plan=plan) # no positional recipient
+ assert quoted.plan is plan and quoted.plan.sender == SENDER
+ assert quoted.total_lamports == 5 + EXPECTED_IGP_PAYMENT_LAMPORTS + NETWORK_FEE_LAMPORTS + RENT
+
+
+def test_a_plan_is_mutually_exclusive_with_sender_and_a_differing_amount():
+ mod, _ = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=5, sender=SENDER).plan
+ with pytest.raises(ValueError, match="plan"):
+ mod.quote_transfer_remote(plan=plan, sender=SENDER)
+ with pytest.raises(ValueError, match="plan"):
+ mod.quote_transfer_remote(plan=plan, amount_atomic=6)
+ assert mod.quote_transfer_remote(plan=plan, amount_atomic=5).plan is plan # identical is fine
+
+
+def test_neither_a_plan_nor_a_recipient_is_refused():
+ mod, fake = module()
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ mod.quote_transfer_remote(amount_atomic=1, sender=SENDER)
+ with pytest.raises(InvalidRecipientError, match="recipient is required when no plan is given"):
+ mod.transfer_remote(amount_atomic=1)
+ assert fake.sent == []
+
+
+def test_a_malformed_json_private_key_never_carries_the_secret_into_the_traceback():
+ """A chained JSONDecodeError keeps the whole document in .doc — which IS the private key."""
+ from aleo_bridge.sol import keypair_from_private_key
+
+ secret = "[17,42,99,128" # a truncated solana-cli id.json
+ with pytest.raises(ConfigurationError) as excinfo:
+ keypair_from_private_key(secret)
+ exc = excinfo.value
+ assert exc.__cause__ is None and exc.__context__ is None
+ assert "17" not in repr(exc) and secret not in repr(exc)
+
+
+def test_a_malformed_base58_private_key_never_carries_the_secret_into_the_traceback():
+ from aleo_bridge.sol import keypair_from_private_key
+
+ secret = "5JueXBoJHvOoPeKeYsEcReT"
+ with pytest.raises(ConfigurationError) as excinfo:
+ keypair_from_private_key(secret)
+ exc = excinfo.value
+ assert exc.__cause__ is None and exc.__context__ is None
+ assert secret not in repr(exc)
+
+
+def test_balance_reads_the_connected_wallet():
+ keypair = Keypair()
+ mod, fake = module(FakeSolanaClient(balance=42), signer=keypair)
+ assert mod.balance() == 42
+ read_only, _ = module()
+ with pytest.raises(ConfigurationError, match="read-only"):
+ read_only.balance()
diff --git a/bridge-sdk/tests/test_sol_rpc.py b/bridge-sdk/tests/test_sol_rpc.py
new file mode 100644
index 0000000..a0eb8c4
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_rpc.py
@@ -0,0 +1,186 @@
+import base64
+import json
+
+import pytest
+
+pytest.importorskip("solders")
+from solders.hash import Hash
+from solders.keypair import Keypair
+from solders.message import MessageV0, to_bytes_versioned
+from solders.pubkey import Pubkey
+from solders.signature import Signature
+from solders.compute_budget import set_compute_unit_limit
+
+from aleo_bridge import sol
+from aleo_bridge.errors import BridgeError
+
+SIG = str(Signature.from_bytes(bytes([9]) * 64))
+ADDR = "4LZtvKvBAM8Hcf5tuL5R7xYj9JC12v6ho8igDnwzo6WC"
+HASH = "8YGT2pZwyZe94qBpGzWfY2TMEVcwaQ1bXAE7YAgpUaM7"
+
+
+class FakeResponse:
+ def __init__(self, body, status_code=200, invalid_json=False):
+ self._body, self.status_code, self._invalid = body, status_code, invalid_json
+
+ def json(self):
+ if self._invalid:
+ raise ValueError("bad json")
+ return self._body
+
+
+class FakeSession:
+ """Answers each POST from a queue of bodies (the last repeats) and records every request."""
+
+ def __init__(self, *bodies, status_code=200, invalid_json=False):
+ self.bodies = list(bodies) or [{"jsonrpc": "2.0", "id": 1, "result": None}]
+ self.status_code, self.invalid_json = status_code, invalid_json
+ self.calls: list[dict] = []
+
+ def post(self, url, *, json, timeout, headers):
+ self.calls.append({"url": url, "json": json, "timeout": timeout, "headers": headers})
+ body = self.bodies.pop(0) if len(self.bodies) > 1 else self.bodies[0]
+ return FakeResponse(body, self.status_code, self.invalid_json)
+
+ def last(self):
+ return self.calls[-1]["json"]["method"], self.calls[-1]["json"]["params"]
+
+
+def ok(result):
+ return {"jsonrpc": "2.0", "id": 1, "result": result}
+
+
+def ctx(value):
+ return ok({"context": {"slot": 1}, "value": value})
+
+
+def client(*bodies, **kw):
+ session = FakeSession(*bodies, **kw)
+ return sol.SolanaRpcClient("http://rpc.test", session=session), session
+
+
+def test_request_envelope_and_defaults():
+ rpc, session = client(ok(42))
+ assert rpc.get_block_height().value == 42
+ call = session.calls[0]
+ assert call["url"] == "http://rpc.test" and call["timeout"] == 30.0
+ assert call["json"]["jsonrpc"] == "2.0" and call["headers"]["cache-control"] == "no-cache"
+ assert session.last() == ("getBlockHeight", [{"commitment": "confirmed"}])
+ assert rpc.commitment == "confirmed" and rpc.url == "http://rpc.test"
+
+
+def test_latest_blockhash():
+ rpc, session = client(ctx({"blockhash": HASH, "lastValidBlockHeight": 123456789}))
+ value = rpc.get_latest_blockhash().value
+ assert value.blockhash == Hash.from_string(HASH) and value.last_valid_block_height == 123456789
+ assert session.last() == ("getLatestBlockhash", [{"commitment": "confirmed"}])
+ rpc2, _ = client(ctx({"blockhash": "", "lastValidBlockHeight": 1}))
+ with pytest.raises(BridgeError, match="getLatestBlockhash returned an invalid result"):
+ rpc2.get_latest_blockhash()
+
+
+def test_balance_and_rent_and_blockhash_validity():
+ rpc, session = client(ctx(1_000_000_000))
+ assert rpc.get_balance(Pubkey.from_string(ADDR)).value == 1_000_000_000
+ assert session.last() == ("getBalance", [ADDR, {"commitment": "confirmed"}])
+ rpc, session = client(ok(890_880))
+ assert rpc.get_minimum_balance_for_rent_exemption(0).value == 890_880
+ assert session.last() == ("getMinimumBalanceForRentExemption", [0, {"commitment": "confirmed"}])
+ with pytest.raises(BridgeError, match="non-negative integer"):
+ rpc.get_minimum_balance_for_rent_exemption(-1)
+ rpc, session = client(ctx(True))
+ assert rpc.is_blockhash_valid(Hash.from_string(HASH)).value is True
+ assert session.last() == ("isBlockhashValid", [HASH, {"commitment": "confirmed"}])
+ rpc, _ = client(ctx(-1))
+ with pytest.raises(BridgeError, match="getBalance returned an invalid result"):
+ rpc.get_balance(Pubkey.from_string(ADDR))
+
+
+def test_account_info():
+ encoded = base64.b64encode(bytes([1, 2, 3, 4])).decode()
+ rpc, session = client(ctx({"data": [encoded, "base64"], "owner": ADDR, "lamports": 1, "executable": False, "rentEpoch": 0}))
+ account = rpc.get_account_info(Pubkey.from_string(ADDR)).value
+ assert account.data == bytes([1, 2, 3, 4]) and account.lamports == 1 and account.owner == ADDR
+ assert session.last() == ("getAccountInfo", [ADDR, {"encoding": "base64", "commitment": "confirmed"}])
+ rpc, _ = client(ctx(None))
+ assert rpc.get_account_info(Pubkey.from_string(ADDR)).value is None
+ rpc, _ = client(ctx({"data": ["abc", "base58"], "owner": ADDR, "lamports": 1}))
+ with pytest.raises(BridgeError, match="invalid base64 account data"):
+ rpc.get_account_info(Pubkey.from_string(ADDR))
+
+
+def test_fee_for_message_sends_the_versioned_message_bytes():
+ payer = Keypair()
+ message = MessageV0.try_compile(payer.pubkey(), [set_compute_unit_limit(400_000)], [], Hash.from_string(HASH))
+ rpc, session = client(ctx(10_000))
+ assert rpc.get_fee_for_message(message).value == 10_000
+ method, params = session.last()
+ assert method == "getFeeForMessage"
+ assert params == [base64.b64encode(to_bytes_versioned(message)).decode(), {"commitment": "confirmed"}]
+ rpc, _ = client(ctx(None))
+ assert rpc.get_fee_for_message(message).value is None
+
+
+def test_send_raw_transaction_params_and_preflight_error_details():
+ rpc, session = client(ok(SIG))
+ assert rpc.send_raw_transaction(b"\x01\x02\x03").value == Signature.from_string(SIG)
+ method, params = session.last()
+ assert method == "sendTransaction"
+ assert params == [base64.b64encode(b"\x01\x02\x03").decode(), {"encoding": "base64", "skipPreflight": False, "preflightCommitment": "confirmed"}]
+ rpc, session = client(ok(SIG))
+ rpc.send_raw_transaction(b"\x01", sol.SendOptions(skip_preflight=True, preflight_commitment="processed"))
+ assert session.last()[1][1] == {"encoding": "base64", "skipPreflight": True, "preflightCommitment": "processed"}
+ failing = {"jsonrpc": "2.0", "id": 1, "error": {"code": -32002, "message": "Transaction simulation failed",
+ "data": {"err": {"InstructionError": [0, "Custom"]}, "logs": ["Program log: insufficient lamports"]}}}
+ rpc, _ = client(failing)
+ with pytest.raises(BridgeError, match="insufficient lamports"):
+ rpc.send_raw_transaction(b"\x01")
+
+
+def test_signature_statuses():
+ rpc, session = client(ctx([None]))
+ assert rpc.get_signature_statuses([Signature.from_string(SIG)], search_transaction_history=True).value == [None]
+ assert session.last() == ("getSignatureStatuses", [[SIG], {"searchTransactionHistory": True}])
+ rpc, _ = client(ctx([{"err": {"InstructionError": [0, "Custom"]}, "confirmationStatus": "processed", "slot": 1, "confirmations": None}]))
+ status = rpc.get_signature_statuses([Signature.from_string(SIG)]).value[0]
+ assert status.err == {"InstructionError": [0, "Custom"]} and status.confirmation_status == "processed"
+ rpc, _ = client(ctx([{"err": None, "confirmationStatus": "finalized", "slot": 1, "confirmations": None}]))
+ assert rpc.get_signature_statuses([Signature.from_string(SIG)]).value[0].confirmation_status == "finalized"
+ rpc, _ = client(ctx([{"err": None, "confirmationStatus": "mystery", "slot": 1}]))
+ with pytest.raises(BridgeError, match="unsupported confirmation status"):
+ rpc.get_signature_statuses([Signature.from_string(SIG)])
+ rpc, _ = client(ctx([{}]))
+ with pytest.raises(BridgeError, match="invalid status"):
+ rpc.get_signature_statuses([Signature.from_string(SIG)])
+
+
+def test_get_transaction():
+ rpc, session = client(ok({"slot": 5, "meta": {"logMessages": ["Program log: hi"]}, "transaction": {}}))
+ value = rpc.get_transaction(Signature.from_string(SIG), max_supported_transaction_version=0).value
+ assert value.slot == 5 and value.transaction.meta.log_messages == ["Program log: hi"]
+ assert session.last() == ("getTransaction", [SIG, {"encoding": "json", "commitment": "confirmed", "maxSupportedTransactionVersion": 0}])
+ rpc, _ = client(ok(None))
+ assert rpc.get_transaction(Signature.from_string(SIG)).value is None
+ rpc, _ = client(ok({"slot": 5, "meta": None, "transaction": {}}))
+ assert rpc.get_transaction(Signature.from_string(SIG)).value.transaction.meta is None
+ rpc, _ = client(ok({"slot": 5, "meta": {"logMessages": "not-a-list"}}))
+ with pytest.raises(BridgeError, match="invalid logs"):
+ rpc.get_transaction(Signature.from_string(SIG))
+
+
+def test_transport_errors_are_bridge_errors():
+ rpc, _ = client({}, status_code=500)
+ with pytest.raises(BridgeError, match="500"):
+ rpc.get_block_height()
+ rpc, _ = client({"jsonrpc": "2.0", "id": 1, "error": {"code": -32602, "message": "Invalid param"}})
+ with pytest.raises(BridgeError, match="Invalid param"):
+ rpc.get_block_height()
+ rpc, _ = client({}, invalid_json=True)
+ with pytest.raises(BridgeError, match="invalid JSON"):
+ rpc.get_block_height()
+ rpc, _ = client({"jsonrpc": "2.0", "id": 1})
+ with pytest.raises(BridgeError, match="invalid result envelope"):
+ rpc.get_block_height()
+ rpc, _ = client(ok("1"))
+ with pytest.raises(BridgeError, match="invalid result"):
+ rpc.get_block_height()
diff --git a/bridge-sdk/tests/test_sol_send.py b/bridge-sdk/tests/test_sol_send.py
new file mode 100644
index 0000000..69bb739
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_send.py
@@ -0,0 +1,433 @@
+import dataclasses
+
+import pytest
+
+pytest.importorskip("solders")
+from solders.keypair import Keypair
+from solders.message import to_bytes_versioned
+from solders.pubkey import Pubkey
+from solders.signature import Signature
+from solders.transaction import VersionedTransaction
+
+from aleo_bridge import _sealevel as sl
+from aleo_bridge import sol
+from aleo_bridge._calls import SolCall
+from aleo_bridge.errors import (
+ BridgeError,
+ ConfigurationError,
+ InsufficientBalanceError,
+ RegistryVersionMismatchError,
+ UnsupportedRouteError,
+)
+from aleo_bridge.sol import SolModule, Solana
+from aleo_bridge.types import DispatchReceipt, Status
+from tests.fakes.fake_solana import BLOCKHASH, STUB_SIGNATURE, FakeSignatureStatus, FakeSolanaClient, stub_bridge
+from tests.fakes.sealevel_fixtures import EXPECTED_MESSAGE_ID, TRANSFER, WARP_PROGRAM_ADDRESS
+
+RECIPIENT = TRANSFER["recipientAleoAddress"]
+AMOUNT = TRANSFER["amountLamports"]
+CHECKPOINT_SOURCE_KEYS = {"transactionId", "blockhash", "lastValidBlockHeight"}
+OTHER_WARP_PROGRAM_ADDRESS = "6HCbFm2P3NWG8SKhzvMLgQHBJAjZvBrbfP6uFtQKpyfd" # a redeployed warp route
+
+
+@pytest.fixture(autouse=True)
+def no_sleep(monkeypatch):
+ monkeypatch.setattr(sol.time, "sleep", lambda seconds: None)
+
+
+def module(fake=None, *, signer=None, checkpoints=None):
+ fake = fake or FakeSolanaClient()
+ keypair = signer or Keypair()
+ bridge = stub_bridge(checkpoints=checkpoints)
+ return SolModule(bridge, Solana(client=fake, signer=keypair)), fake, keypair
+
+
+class ExplodingStore:
+ """A checkpoint store whose disk is full / read-only."""
+
+ def __init__(self):
+ self.attempts = []
+
+ def save(self, checkpoint):
+ self.attempts.append(checkpoint)
+ raise OSError("read-only file system")
+
+ def load(self, checkpoint_id): # pragma: no cover - never reached
+ return None
+
+ def list(self): # pragma: no cover - never reached
+ return []
+
+ def delete(self, checkpoint_id): # pragma: no cover - never reached
+ return None
+
+
+def test_extract_message_id_reads_only_the_mailbox_dispatch_line():
+ assert sl.extract_hyperlane_message_id(TRANSFER["logMessages"]) == EXPECTED_MESSAGE_ID
+ truncated = [line for line in TRANSFER["logMessages"] if "Dispatched message" not in line]
+ assert any("Paid IGP" in line for line in truncated)
+ assert sl.extract_hyperlane_message_id(truncated) is None
+ assert sl.extract_hyperlane_message_id(None) is None
+ assert sl.extract_hyperlane_message_id([]) is None
+
+
+def test_extract_message_id_refuses_an_over_long_hex_id():
+ """A 65+-hex id is not a 32-byte message id; truncating it to 64 would report a plausible wrong id."""
+ over_long = "Program log: Dispatched message to 1634493807, ID 0x" + "a" * 65
+ assert sl.extract_hyperlane_message_id([over_long]) is None
+ exact = "Program log: Dispatched message to 1634493807, ID 0x" + "a" * 64
+ assert sl.extract_hyperlane_message_id([exact]) == "0x" + "a" * 64
+
+
+def test_build_returns_a_transaction_signed_only_by_the_unique_message_key():
+ mod, fake, keypair = module()
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=AMOUNT)
+ assert isinstance(call, SolCall)
+ tx = call.build()
+ assert isinstance(tx, VersionedTransaction)
+ message = tx.message
+ assert message.header.num_required_signatures == 2
+ assert message.account_keys[0] == keypair.pubkey() # fee payer first
+ unique = Pubkey.from_string(call.quote.unique_message_address)
+ assert message.account_keys[1] == unique
+ assert tx.signatures[0] == Signature.default() # fee payer unsigned
+ assert tx.signatures[1] != Signature.default()
+ assert tx.signatures[1].verify(unique, to_bytes_versioned(message))
+ programs = [str(message.account_keys[ix.program_id_index]) for ix in message.instructions]
+ assert programs == ["ComputeBudget111111111111111111111111111111", WARP_PROGRAM_ADDRESS]
+ assert fake.sent == [] and call.quote.total_lamports == 676_207_914_240
+
+
+def test_send_adds_the_fee_payer_signature_confirms_and_extracts_the_message_id():
+ mod, fake, keypair = module()
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=AMOUNT).send()
+ assert isinstance(result, DispatchReceipt)
+ signature = fake.sent_signature()
+ assert result.transaction_id == signature and result.route_id == sl.SOLANA_ROUTE_ID
+ assert result.message_id == EXPECTED_MESSAGE_ID and result.amount_atomic == AMOUNT
+ receipt = result.receipt
+ assert receipt.status is Status.DELIVERY_PENDING and receipt.id == EXPECTED_MESSAGE_ID
+ assert receipt.source_tx_id == signature and receipt.protocol == "hyperlane"
+ assert receipt.protocol_state["messageId"] == EXPECTED_MESSAGE_ID
+ assert "messageIdUnavailable" not in receipt.protocol_state
+ assert len(fake.sent) == 1
+ tx = VersionedTransaction.from_bytes(fake.sent[0])
+ assert all(signature != Signature.default() for signature in tx.signatures)
+ assert tx.signatures[0].verify(keypair.pubkey(), to_bytes_versioned(tx.message))
+ tx.verify_and_hash_message() # raises if any signature is invalid
+ opts = fake.sent_opts[0]
+ assert isinstance(opts, sol.SendOptions)
+ assert opts.skip_preflight is False and opts.preflight_commitment == "confirmed" and opts.skip_confirmation is True
+ assert fake.status_calls == [True] # searchTransactionHistory
+ assert fake.transaction_calls == [("confirmed", 0)] # confirmed commitment, v0
+
+
+def test_send_with_a_non_keypair_signer():
+ inner = Keypair()
+
+ class RemoteSigner:
+ def pubkey(self):
+ return inner.pubkey()
+
+ def sign_message(self, message: bytes):
+ return inner.sign_message(message)
+
+ fake = FakeSolanaClient()
+ mod = SolModule(stub_bridge(), Solana(client=fake, signer=RemoteSigner()))
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send()
+ tx = VersionedTransaction.from_bytes(fake.sent[0])
+ assert tx.signatures[0].verify(inner.pubkey(), to_bytes_versioned(tx.message))
+ assert result.receipt.status is Status.DELIVERY_PENDING
+
+
+def test_send_refuses_a_plan_prepared_for_another_sender_before_any_read():
+ mod, fake, _ = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=1, sender=TRANSFER["senderAddress"]).plan
+ fake.calls.clear()
+ with pytest.raises(ConfigurationError, match=f"Prepared sender {TRANSFER['senderAddress']} does not match connected account"):
+ mod.transfer_remote(RECIPIENT, amount_atomic=1, plan=plan).send()
+ assert fake.calls == [] and fake.sent == []
+
+
+def test_send_refuses_a_stale_or_foreign_plan():
+ mod, fake, _ = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=1).plan
+ stale = dataclasses.replace(plan, registry_version="2020-01-01.stale.0")
+ with pytest.raises(RegistryVersionMismatchError):
+ mod.transfer_remote(RECIPIENT, plan=stale).send()
+ foreign = dataclasses.replace(plan, route_id="hyperlane:ethereum/eth->aleo/eth")
+ with pytest.raises(UnsupportedRouteError):
+ mod.transfer_remote(RECIPIENT, plan=foreign).send()
+ assert fake.sent == []
+
+
+def test_send_uses_the_plan_when_it_matches_the_wallet():
+ mod, fake, keypair = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=3).plan
+ assert plan.sender == str(keypair.pubkey())
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=3, plan=plan).send()
+ assert result.receipt.protocol_state["quotedLamports"] == str(3 + 2_900_000 + 10_000 + 5_004_240)
+
+
+def test_send_insufficient_balance_names_the_amount_gas_and_rent_split():
+ mod, fake, _ = module(FakeSolanaClient(balance=0))
+ with pytest.raises(InsufficientBalanceError) as excinfo:
+ mod.transfer_remote(RECIPIENT, amount_atomic=AMOUNT).send()
+ message = str(excinfo.value)
+ for fragment in ("balance 0 lamports", "required 676207914240 lamports", "amount 676200000000", "gas 2910000", "rent 5004240"):
+ assert fragment in message
+ assert fake.sent == []
+
+
+def test_send_checkpoints_source_confirming_before_the_first_status_read():
+ mod, fake, _ = module()
+ seen = []
+
+ def on_checkpoint(checkpoint):
+ fake.calls.append("checkpoint")
+ seen.append(checkpoint)
+
+ mod.transfer_remote(RECIPIENT, amount_atomic=AMOUNT).send(on_checkpoint=on_checkpoint)
+ assert len(seen) == 1
+ checkpoint = seen[0]
+ assert checkpoint.id == fake.sent_signature()
+ assert checkpoint.route == {"id": sl.SOLANA_ROUTE_ID, "registryVersion": mod.registry.version}
+ assert set(checkpoint.source) == CHECKPOINT_SOURCE_KEYS
+ assert checkpoint.source["transactionId"] == fake.sent_signature()
+ assert checkpoint.source["blockhash"] == str(BLOCKHASH) and checkpoint.source["lastValidBlockHeight"] == "100"
+ assert fake.calls.index("send_raw_transaction") < fake.calls.index("checkpoint") < fake.calls.index("get_signature_statuses")
+
+
+def test_bound_store_saves_the_source_checkpoint_after_the_caller_callback():
+ saved = []
+
+ class RecordingStore:
+ def save(self, checkpoint):
+ saved.append(checkpoint)
+
+ def load(self, checkpoint_id):
+ return None
+
+ def list(self):
+ return []
+
+ def delete(self, checkpoint_id):
+ return None
+
+ mod, fake, _ = module(checkpoints=RecordingStore())
+ seen = []
+ mod.transfer_remote(RECIPIENT, amount_atomic=1).send(on_checkpoint=seen.append)
+ assert [cp.id for cp in saved] == [fake.sent_signature()] == [cp.id for cp in seen]
+
+
+def test_store_failure_after_broadcast_reports_the_signature_and_never_hides_it():
+ """The transaction is already on the wire: the caller's callback must have run first, the
+ error must name the signature and the checkpoint, and no status poll may follow the failure."""
+ store = ExplodingStore()
+ mod, fake, _ = module(checkpoints=store)
+ seen = []
+ with pytest.raises(BridgeError) as excinfo:
+ mod.transfer_remote(RECIPIENT, amount_atomic=1).send(on_checkpoint=seen.append)
+ message = str(excinfo.value)
+ signature = fake.sent_signature()
+ assert signature in message and "broadcast" in message and "checkpoint" in message.lower()
+ assert [cp.id for cp in seen] == [signature] # callback ran before the store
+ assert [cp.id for cp in store.attempts] == [signature]
+ assert len(fake.sent) == 1 # broadcast happened exactly once
+ assert "get_signature_statuses" not in fake.calls # nothing polled after the failure
+
+
+def test_send_failed_status_raises_naming_the_signature():
+ mod, fake, _ = module(FakeSolanaClient(statuses=[FakeSignatureStatus(err={"InstructionError": [2, "Custom"]})]))
+ with pytest.raises(BridgeError) as excinfo:
+ mod.transfer_remote(RECIPIENT, amount_atomic=1).send()
+ assert fake.sent_signature() in str(excinfo.value)
+
+
+def test_send_timeout_returns_a_pending_source_confirming_receipt():
+ mod, fake, _ = module(FakeSolanaClient(statuses=[None]))
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(timeout_seconds=0)
+ receipt = result.receipt
+ assert receipt.status is Status.SOURCE_CONFIRMING and receipt.id == fake.sent_signature()
+ assert result.message_id is None and "messageId" not in receipt.protocol_state
+ assert "get_transaction" not in fake.calls and len(fake.sent) == 1
+
+
+def test_send_expired_blockhash_returns_expired_without_resubmitting():
+ mod, fake, _ = module(FakeSolanaClient(statuses=[None], blockhash_valid=False))
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send()
+ receipt = result.receipt
+ assert receipt.status is Status.EXPIRED
+ assert receipt.protocol_state["blockhashExpired"] is True
+ assert fake.sent_signature() in receipt.protocol_state["sourceError"]
+ assert len(fake.sent) == 1
+
+
+def test_processed_is_never_reported_expired_and_skips_the_blockhash_probe():
+ """A processed transaction has landed; calling it EXPIRED would invite a resend (double spend)."""
+ fake = FakeSolanaClient(statuses=[FakeSignatureStatus(confirmation_status="processed")], blockhash_valid=False)
+ mod, _, _ = module(fake)
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(timeout_seconds=0)
+ receipt = result.receipt
+ assert receipt.status is Status.SOURCE_CONFIRMING and receipt.id == fake.sent_signature()
+ assert "blockhashExpired" not in receipt.protocol_state
+ assert "is_blockhash_valid" not in fake.calls
+ assert len(fake.sent) == 1
+
+
+def test_a_status_row_without_a_confirmation_level_is_never_reported_expired():
+ """err=None with no confirmationStatus means the transaction landed; only a MISSING row is unknown."""
+ fake = FakeSolanaClient(statuses=[FakeSignatureStatus(err=None, confirmation_status=None)], blockhash_valid=False)
+ mod, _, _ = module(fake)
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(timeout_seconds=0)
+ receipt = result.receipt
+ assert receipt.status is Status.SOURCE_CONFIRMING and receipt.id == fake.sent_signature()
+ assert "blockhashExpired" not in receipt.protocol_state
+ assert "is_blockhash_valid" not in fake.calls and len(fake.sent) == 1
+
+
+def test_log_fetch_failure_after_confirmation_degrades_to_message_id_unavailable():
+ """The logs only carry the message id: an RPC failure there must not fail a settled transfer."""
+ fake = FakeSolanaClient(get_transaction_error=RuntimeError("rpc"))
+ mod, _, _ = module(fake)
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send()
+ receipt = result.receipt
+ assert receipt.status is Status.DELIVERY_PENDING and receipt.id == fake.sent_signature()
+ assert receipt.protocol_state["messageIdUnavailable"] is True
+ assert "messageId" not in receipt.protocol_state and result.message_id is None
+ assert len(fake.sent) == 1
+
+
+def test_send_swallows_transient_status_read_errors():
+ fake = FakeSolanaClient(statuses=[RuntimeError("rate limited"), RuntimeError("again"), FakeSignatureStatus()], blockhash_valid=RuntimeError("advisory"))
+ mod, _, _ = module(fake)
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(timeout_seconds=5, poll_seconds=0)
+ assert result.receipt.status is Status.DELIVERY_PENDING and len(fake.status_calls) >= 3
+
+
+def test_send_persistent_status_errors_time_out_to_pending():
+ mod, _, _ = module(FakeSolanaClient(statuses=[RuntimeError("down")]))
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(timeout_seconds=0)
+ assert result.receipt.status is Status.SOURCE_CONFIRMING
+
+
+def test_send_without_wait_skips_polling():
+ mod, fake, _ = module()
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send(wait=False)
+ assert result.receipt.status is Status.SOURCE_CONFIRMING and "get_signature_statuses" not in fake.calls
+
+
+def test_finalized_counts_as_confirmed_and_missing_log_marks_message_id_unavailable():
+ mod, fake, _ = module(FakeSolanaClient(statuses=[FakeSignatureStatus(confirmation_status="finalized")], logs=[]))
+ result = mod.transfer_remote(RECIPIENT, amount_atomic=1).send()
+ receipt = result.receipt
+ assert receipt.status is Status.DELIVERY_PENDING and receipt.id == fake.sent_signature()
+ assert receipt.protocol_state["messageIdUnavailable"] is True and result.message_id is None
+ processed_then_confirmed = FakeSolanaClient(statuses=[FakeSignatureStatus(confirmation_status="processed"), FakeSignatureStatus()])
+ mod2, _, _ = module(processed_then_confirmed)
+ assert mod2.transfer_remote(RECIPIENT, amount_atomic=1).send(poll_seconds=0).receipt.status is Status.DELIVERY_PENDING
+
+
+def test_send_requires_a_signer():
+ fake = FakeSolanaClient()
+ read_only = SolModule(stub_bridge(), Solana(client=fake))
+ with pytest.raises(ConfigurationError, match="read-only"):
+ read_only.transfer_remote(RECIPIENT, amount_atomic=1).build()
+ assert fake.sent == []
+
+
+def test_transfer_remote_accepts_a_plan_without_a_recipient():
+ mod, fake, keypair = module()
+ plan = mod.quote_transfer_remote(RECIPIENT, amount_atomic=3).plan
+ result = mod.transfer_remote(plan=plan).send() # no positional recipient
+ assert result.receipt.status is Status.DELIVERY_PENDING and len(fake.sent) == 1
+ with pytest.raises(ValueError, match="plan"):
+ mod.transfer_remote(plan=plan, amount_atomic=4)
+ assert mod.transfer_remote(plan=plan, amount_atomic=3) is not None # identical is fine
+
+
+def test_build_compiles_against_the_re_resolved_route_not_the_one_snapshotted_at_transfer_remote():
+ """The registry can move between transfer_remote() and build(); building the instruction from the
+ stale snapshot while the quote used the fresh route would sign against the wrong program."""
+ mod, fake, _ = module()
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=1)
+ original = mod.outbound_route()
+ moved = dataclasses.replace(original, metadata={**original.metadata,
+ "warpProgramAddress": OTHER_WARP_PROGRAM_ADDRESS})
+ mod.outbound_route = lambda: moved # the live registry now says otherwise
+ transaction = call.build()
+ programs = [str(transaction.message.account_keys[ix.program_id_index]) for ix in transaction.message.instructions]
+ assert OTHER_WARP_PROGRAM_ADDRESS in programs and WARP_PROGRAM_ADDRESS not in programs
+ assert fake.sent == []
+
+
+def test_a_call_is_single_use_once_it_has_broadcast():
+ """Re-sending the same call would sign a second transfer of the same funds."""
+ mod, fake, _ = module()
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=1)
+ call.send()
+ signature = fake.sent_signature()
+ with pytest.raises(BridgeError) as excinfo:
+ call.send()
+ message = str(excinfo.value)
+ assert message == (f"this call already broadcast {signature}; use bridge.sol.source_status(plan, receipt) "
+ "to follow it — do not resend")
+ assert len(fake.sent) == 1
+
+
+def test_a_lost_send_response_also_arms_the_single_use_guard():
+ """The bytes may be on the wire; a resend is exactly what must not happen next."""
+ fake = FakeSolanaClient(send_error=RuntimeError("connection reset by peer"))
+ mod, _, _ = module(fake)
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=1)
+ with pytest.raises(BridgeError, match="may have been broadcast"):
+ call.send()
+ with pytest.raises(BridgeError, match="already broadcast"):
+ call.send()
+ assert len(fake.sent) == 1
+
+
+def test_a_failure_before_any_broadcast_leaves_the_call_usable():
+ fake = FakeSolanaClient(balance=0)
+ mod, _, _ = module(fake)
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=1)
+ with pytest.raises(InsufficientBalanceError):
+ call.send()
+ assert fake.sent == []
+ fake.balance = 800_000_000_000
+ result = call.send() # the guard was never armed
+ assert result.receipt.status is Status.DELIVERY_PENDING and len(fake.sent) == 1
+
+
+def test_build_stays_repeatable_after_a_send():
+ mod, fake, _ = module()
+ call = mod.transfer_remote(RECIPIENT, amount_atomic=1)
+ call.send()
+ assert isinstance(call.build(), VersionedTransaction) # a preview never spends
+ assert len(fake.sent) == 1
+
+
+def test_a_lost_send_response_names_the_local_signature_and_never_polls_or_checkpoints():
+ """The bytes may already be on the wire: losing the RPC answer must not lose the signature."""
+ fake = FakeSolanaClient(send_error=RuntimeError("connection reset by peer"))
+ mod, _, _ = module(fake)
+ seen = []
+ with pytest.raises(BridgeError) as excinfo:
+ mod.transfer_remote(RECIPIENT, amount_atomic=1).send(on_checkpoint=seen.append)
+ message = str(excinfo.value)
+ assert fake.sent_signature() in message # the id the caller needs to investigate
+ assert "may have been broadcast" in message and "connection reset by peer" in message
+ assert fake.calls.count("send_raw_transaction") == 1 # exactly one attempt, never retried
+ assert "get_signature_statuses" not in fake.calls and seen == []
+
+
+def test_a_node_signature_that_differs_from_the_signed_one_is_refused_without_checkpointing():
+ """Checkpointing the node's id would follow — and later resend — the wrong transaction."""
+ fake = FakeSolanaClient(signature=STUB_SIGNATURE)
+ mod, _, _ = module(fake)
+ seen = []
+ with pytest.raises(BridgeError) as excinfo:
+ mod.transfer_remote(RECIPIENT, amount_atomic=1).send(on_checkpoint=seen.append)
+ message = str(excinfo.value)
+ assert fake.sent_signature() in message and str(STUB_SIGNATURE) in message
+ assert seen == [] and "get_signature_statuses" not in fake.calls
diff --git a/bridge-sdk/tests/test_sol_status.py b/bridge-sdk/tests/test_sol_status.py
new file mode 100644
index 0000000..7e9b71c
--- /dev/null
+++ b/bridge-sdk/tests/test_sol_status.py
@@ -0,0 +1,106 @@
+import dataclasses
+
+import pytest
+
+pytest.importorskip("solders")
+
+from aleo_bridge.errors import BridgeError, CheckpointInvalidError
+from aleo_bridge.sol import SolModule, Solana
+from aleo_bridge.types import Receipt, Status
+from tests.fakes.fake_solana import BLOCKHASH, STUB_SIGNATURE, FakeSignatureStatus, FakeSolanaClient, stub_bridge
+from tests.fakes.sealevel_fixtures import EXPECTED_MESSAGE_ID, TRANSFER
+
+SIGNATURE = str(STUB_SIGNATURE)
+
+
+def setup(fake=None):
+ fake = fake or FakeSolanaClient()
+ mod = SolModule(stub_bridge(), Solana(client=fake))
+ plan = mod.quote_transfer_remote(TRANSFER["recipientAleoAddress"], amount_atomic=1, sender=TRANSFER["senderAddress"]).plan
+ fake.calls.clear()
+ return mod, fake, plan
+
+
+def receipt(plan, **state) -> Receipt:
+ return Receipt(id=SIGNATURE, protocol="hyperlane", status=Status.SOURCE_CONFIRMING, source_tx_id=SIGNATURE,
+ protocol_state={"routeId": plan.route_id, **state})
+
+
+def test_confirmed_signature_advances_to_delivery_pending_with_message_id():
+ mod, fake, plan = setup()
+ out = mod.source_status(plan, receipt(plan))
+ assert out.status is Status.DELIVERY_PENDING and out.id == EXPECTED_MESSAGE_ID
+ assert out.protocol_state["messageId"] == EXPECTED_MESSAGE_ID and out.source_tx_id == SIGNATURE
+ assert fake.status_calls == [True] and fake.calls == ["get_signature_statuses", "get_transaction"]
+
+
+def test_finalized_without_log_marks_message_id_unavailable():
+ mod, _, plan = setup(FakeSolanaClient(statuses=[FakeSignatureStatus(confirmation_status="finalized")], no_logs=True))
+ out = mod.source_status(plan, receipt(plan))
+ assert out.status is Status.DELIVERY_PENDING and out.id == SIGNATURE and out.protocol_state["messageIdUnavailable"] is True
+
+
+def test_processed_and_unknown_without_lifetime_are_unchanged():
+ mod, _, plan = setup(FakeSolanaClient(statuses=[FakeSignatureStatus(confirmation_status="processed")]))
+ original = receipt(plan)
+ assert mod.source_status(plan, original) == original
+ mod2, fake2, plan2 = setup(FakeSolanaClient(statuses=[None]))
+ assert mod2.source_status(plan2, receipt(plan2)) == receipt(plan2)
+ assert "is_blockhash_valid" not in fake2.calls
+
+
+def test_a_status_row_without_a_confirmation_level_counts_as_processed():
+ """A row with err=None and no confirmationStatus means the transaction EXISTS (processed at
+ least). Treating it as 'unknown' would probe the blockhash and report EXPIRED on a landed
+ transfer — an invitation to resend."""
+ lifetime = {"blockhash": str(BLOCKHASH), "lastValidBlockHeight": "100"}
+ fake = FakeSolanaClient(statuses=[FakeSignatureStatus(err=None, confirmation_status=None)], blockhash_valid=False)
+ mod, _, plan = setup(fake)
+ original = receipt(plan, **lifetime)
+ assert mod.source_status(plan, original) == original
+ assert "is_blockhash_valid" not in fake.calls
+
+
+def test_unknown_signature_with_lifetime_checks_the_blockhash():
+ lifetime = {"blockhash": str(BLOCKHASH), "lastValidBlockHeight": "100"}
+ mod, fake, plan = setup(FakeSolanaClient(statuses=[None], blockhash_valid=True))
+ assert mod.source_status(plan, receipt(plan, **lifetime)) == receipt(plan, **lifetime)
+ assert "is_blockhash_valid" in fake.calls
+ expired_mod, _, plan = setup(FakeSolanaClient(statuses=[None], blockhash_valid=False))
+ out = expired_mod.source_status(plan, receipt(plan, **lifetime))
+ assert out.status is Status.EXPIRED and out.protocol_state["blockhashExpired"] is True
+ assert SIGNATURE in out.protocol_state["sourceError"]
+ flaky_mod, _, plan = setup(FakeSolanaClient(statuses=[None], blockhash_valid=RuntimeError("rpc")))
+ assert flaky_mod.source_status(plan, receipt(plan, **lifetime)) == receipt(plan, **lifetime)
+
+
+def test_malformed_lifetime_is_a_checkpoint_error():
+ mod, _, plan = setup(FakeSolanaClient(statuses=[None]))
+ with pytest.raises(CheckpointInvalidError, match="blockhash lifetime"):
+ mod.source_status(plan, receipt(plan, blockhash=str(BLOCKHASH)))
+ with pytest.raises(CheckpointInvalidError, match="blockhash lifetime"):
+ mod.source_status(plan, receipt(plan, blockhash=str(BLOCKHASH), lastValidBlockHeight="soon"))
+ with pytest.raises(CheckpointInvalidError, match="blockhash lifetime"):
+ mod.source_status(plan, receipt(plan, blockhash="not-base58-0OIl", lastValidBlockHeight="100"))
+
+
+def test_failed_signature_raises_and_status_read_errors_propagate():
+ mod, _, plan = setup(FakeSolanaClient(statuses=[FakeSignatureStatus(err={"InstructionError": [1, "Custom"]})]))
+ with pytest.raises(BridgeError, match=SIGNATURE):
+ mod.source_status(plan, receipt(plan))
+ down, _, plan = setup(FakeSolanaClient(statuses=[RuntimeError("rpc down")]))
+ with pytest.raises(RuntimeError):
+ down.source_status(plan, receipt(plan))
+
+
+def test_guards():
+ mod, _, plan = setup()
+ with pytest.raises(BridgeError, match="source-confirming"):
+ mod.source_status(plan, dataclasses.replace(receipt(plan), status=Status.DELIVERY_PENDING))
+ with pytest.raises(BridgeError, match="source-confirming"):
+ mod.source_status(plan, dataclasses.replace(receipt(plan), source_tx_id=None))
+ with pytest.raises(BridgeError, match="source-confirming"):
+ mod.source_status(plan, dataclasses.replace(receipt(plan), protocol="xreserve"))
+ with pytest.raises(BridgeError, match="does not match"):
+ mod.source_status(plan, Receipt(id=SIGNATURE, protocol="hyperlane", status=Status.SOURCE_CONFIRMING,
+ source_tx_id=SIGNATURE, protocol_state={"routeId": "hyperlane:ethereum/eth->aleo/eth"}))
diff --git a/bridge-sdk/tests/test_types.py b/bridge-sdk/tests/test_types.py
new file mode 100644
index 0000000..9c2a927
--- /dev/null
+++ b/bridge-sdk/tests/test_types.py
@@ -0,0 +1,125 @@
+import dataclasses
+import json
+
+import pytest
+
+import aleo_bridge
+from aleo_bridge import types
+from aleo_bridge.errors import ConfigurationError
+from aleo_bridge.types import (CALLER_BOUNDARIES, TERMINAL, AleoHyperlaneQuote, Attestation, BridgeStatus,
+ ChainStatus, DispatchReceipt, Fee, GasQuote, Plan, PreparedTx, PrivacyReceipt,
+ Progress, Receipt, Status, Step, to_progress)
+
+
+def _plan() -> Plan:
+ return Plan(route_id="hyperlane:aleo/wbtc->ethereum/wbtc", registry_version="2026-08-31.solana-deposits.1",
+ protocol="hyperlane", environment="mainnet", source_asset_id="aleo/wbtc",
+ destination_asset_id="ethereum/wbtc", amount="0.001", amount_atomic=100_000,
+ recipient="0x0000000000000000000000000000000000000001", sender=None, mint_mode="public",
+ steps=(Step("source-dispatch", "dispatch", "aleo-wallet", True),
+ Step("message-delivery", "wait-delivery", "protocol", False)))
+
+
+def test_status_enum_and_sets():
+ assert [s.value for s in Status] == [
+ "PREPARED", "SOURCE_APPROVAL_PENDING", "SOURCE_SUBMISSION_PENDING", "SOURCE_CONFIRMING", "ATTESTATION_PENDING",
+ "DESTINATION_ACTION_REQUIRED", "DELIVERY_PENDING", "DESTINATION_CONFIRMING", "COMPLETED", "FAILED", "EXPIRED"]
+ assert Status.COMPLETED == "COMPLETED" and Status("FAILED") is Status.FAILED
+ assert TERMINAL == {Status.COMPLETED, Status.FAILED, Status.EXPIRED}
+ assert CALLER_BOUNDARIES == {Status.SOURCE_SUBMISSION_PENDING, Status.DESTINATION_ACTION_REQUIRED,
+ Status.COMPLETED, Status.FAILED, Status.EXPIRED}
+ assert json.dumps({"s": Status.COMPLETED}) == '{"s": "COMPLETED"}'
+
+
+@pytest.mark.parametrize("status,expected", [
+ (Status.SOURCE_SUBMISSION_PENDING, "resume"), (Status.DESTINATION_ACTION_REQUIRED, "complete"),
+ (Status.COMPLETED, "done"), (Status.FAILED, "failed"), (Status.EXPIRED, "failed"),
+ (Status.PREPARED, "wait"), (Status.SOURCE_APPROVAL_PENDING, "wait"), (Status.SOURCE_CONFIRMING, "wait"),
+ (Status.ATTESTATION_PENDING, "wait"), (Status.DELIVERY_PENDING, "wait"), (Status.DESTINATION_CONFIRMING, "wait"),
+])
+def test_to_progress_table(status, expected):
+ receipt = Receipt(id="at1x", protocol="hyperlane", status=status, protocol_state={"routeId": "r"})
+ plan = _plan()
+ progress = to_progress(plan, receipt)
+ assert progress.next == expected
+ assert progress.plan is plan and progress.receipt is receipt
+ # Error is only set for FAILED and EXPIRED statuses
+ if status in {Status.FAILED, Status.EXPIRED}:
+ assert progress.error is not None
+ else:
+ assert progress.error is None
+
+
+def test_to_progress_error_derivation():
+ """Test error field is derived from protocol_state for FAILED/EXPIRED statuses."""
+ plan = _plan()
+
+ # FAILED with destinationError (takes priority over sourceError)
+ receipt_de = Receipt(id="at1x", protocol="hyperlane", status=Status.FAILED,
+ protocol_state={"routeId": "r", "destinationError": "dest err", "sourceError": "src err"})
+ assert to_progress(plan, receipt_de).error == "dest err"
+
+ # FAILED with only sourceError
+ receipt_se = Receipt(id="at1x", protocol="hyperlane", status=Status.FAILED,
+ protocol_state={"routeId": "r", "sourceError": "src err only"})
+ assert to_progress(plan, receipt_se).error == "src err only"
+
+ # EXPIRED with neither error field (generates default message)
+ receipt_expired = Receipt(id="at1x", protocol="hyperlane", status=Status.EXPIRED, protocol_state={"routeId": "r"})
+ progress = to_progress(plan, receipt_expired)
+ assert progress.error == "Bridge transfer ended in EXPIRED"
+
+
+def test_to_progress_accepts_status_strings_and_requires_route_id():
+ receipt = Receipt(id="at1x", protocol="hyperlane", status="COMPLETED", protocol_state={"routeId": "r"})
+ assert to_progress(_plan(), receipt).next == "done"
+ with pytest.raises(ConfigurationError, match="routeId"):
+ to_progress(_plan(), Receipt(id="at1x", protocol="hyperlane", status=Status.COMPLETED))
+
+
+def test_plan_round_trip():
+ plan = _plan()
+ d = plan.to_dict()
+ assert d["steps"][0] == {"id": "source-dispatch", "kind": "dispatch", "executor": "aleo-wallet", "irreversible": True}
+ assert Plan.from_dict(json.loads(json.dumps(d))) == plan
+ with pytest.raises(dataclasses.FrozenInstanceError):
+ plan.amount = "2" # type: ignore[misc]
+
+
+def test_receipt_replace_and_defaults():
+ r = Receipt(id="0xabc", protocol="xreserve", status=Status.ATTESTATION_PENDING, protocol_state={"routeId": "x"})
+ assert r.source_tx_id is None and r.destination_tx_id is None and r.next_action is None
+ r2 = r.replace(status=Status.DESTINATION_ACTION_REQUIRED, next_action={"kind": "xreserve-private-mint", "chainId": "aleo"})
+ assert r2.status is Status.DESTINATION_ACTION_REQUIRED and r.status is Status.ATTESTATION_PENDING
+ assert r2.protocol_state == {"routeId": "x"} and r2.next_action["kind"] == "xreserve-private-mint"
+ # replace() must copy protocol_state/next_action, never alias the source's dicts
+ r2.protocol_state["routeId"] = "mutated"
+ r2.next_action["kind"] = "mutated"
+ assert r.protocol_state == {"routeId": "x"} and r.next_action is None
+ r3 = r2.replace(status=Status.COMPLETED) # replace() with neither field explicit still copies, not aliases
+ r3.protocol_state["routeId"] = "again"
+ assert r2.protocol_state["routeId"] == "mutated"
+
+
+def test_result_dataclasses():
+ receipt = Receipt(id="at1x", protocol="hyperlane", status=Status.SOURCE_CONFIRMING, protocol_state={"routeId": "r"})
+ dr = DispatchReceipt(transaction_id="at1x", route_id="r", message_id=None, amount_atomic=1, receipt=receipt)
+ assert dr.receipt.protocol_state["routeId"] == "r"
+ assert GasQuote("r", 44000, 159337, 1000000000, 402, 8174147).payment_microcredits == 8174147
+ att = Attestation(payload=bytes(305), message_hash=bytes(32), attestation=bytes(65), status="complete")
+ assert len(att.payload) == 305
+ assert PreparedTx("at1x", "{}").serialized == "{}"
+ assert PrivacyReceipt("at1s", "aleo/eth", "0.000000000000000001", 1, "shield").direction == "shield"
+ q = AleoHyperlaneQuote(kind="aleo-hyperlane", plan=_plan(), fees=(Fee("hook", "aleo", "aleo/aleo", "8.174147", False),),
+ amount_out="0.001", gas_limit=44000, gas_overhead=159337, gas_price=1000000000,
+ exchange_rate=402, payment_microcredits=8174147)
+ assert q.fees[0].estimated is False and q.kind == "aleo-hyperlane"
+ status = BridgeStatus(environment="mainnet", registry_version="v", chains=[ChainStatus("aleo", None, False, {})], pending=[])
+ assert status.chains[0].can_sign is False
+ assert isinstance(Progress("wait", _plan(), receipt), Progress)
+
+
+def test_all_types_exported_from_package():
+ """Ensure all names in types.__all__ are re-exported from aleo_bridge.__all__."""
+ assert set(types.__all__) <= set(aleo_bridge.__all__), \
+ f"Missing exports: {set(types.__all__) - set(aleo_bridge.__all__)}"
diff --git a/bridge-sdk/tests/test_units.py b/bridge-sdk/tests/test_units.py
new file mode 100644
index 0000000..2f4ecfc
--- /dev/null
+++ b/bridge-sdk/tests/test_units.py
@@ -0,0 +1,60 @@
+from decimal import Decimal
+
+import pytest
+
+from aleo_bridge.errors import InvalidAmountError
+from aleo_bridge.units import format_decimal_amount, parse_decimal_amount, resolve_amount
+
+
+@pytest.mark.parametrize("amount,decimals,expected", [
+ ("100", 6, 100_000_000), ("0.5", 6, 500_000), ("1.5", 6, 1_500_000), ("0.01", 8, 1_000_000),
+ ("1.000000000000000001", 18, 10**18 + 1), ("42", 0, 42), ("0.123456", 6, 123_456),
+ (Decimal("0.5"), 6, 500_000), (7, 6, 7_000_000), (" 2 ", 6, 2_000_000),
+])
+def test_parse_decimal_amount(amount, decimals, expected):
+ assert parse_decimal_amount(amount, decimals) == expected
+
+
+@pytest.mark.parametrize("amount", ["0.1234567", "1e6", "-1", "1.", "", ".5", "1,5", "abc"])
+def test_parse_decimal_amount_rejects(amount):
+ with pytest.raises(InvalidAmountError):
+ parse_decimal_amount(amount, 6)
+
+
+def test_parse_decimal_amount_rejects_unicode_digits():
+ # Python's \d matches any Unicode decimal digit, not just ASCII 0-9; the regex must be [0-9] only.
+ with pytest.raises(InvalidAmountError):
+ parse_decimal_amount("٥٦", 6) # ARABIC-INDIC DIGIT FIVE/SIX — category Nd, not ASCII
+
+
+def test_parse_decimal_amount_rejects_bad_types_and_decimals():
+ with pytest.raises(InvalidAmountError):
+ parse_decimal_amount(1.5, 6) # type: ignore[arg-type]
+ with pytest.raises(InvalidAmountError):
+ parse_decimal_amount(True, 6) # type: ignore[arg-type]
+ with pytest.raises(InvalidAmountError):
+ parse_decimal_amount("1", -1)
+
+
+def test_format_decimal_amount():
+ assert format_decimal_amount(2_000_001, 6) == "2.000001"
+ assert format_decimal_amount(1_500_000, 6) == "1.5"
+ assert format_decimal_amount(100_000_000, 6) == "100"
+ assert format_decimal_amount(1, 18) == "0.000000000000000001"
+ assert format_decimal_amount(0, 6) == "0"
+ assert format_decimal_amount(42, 0) == "42"
+ with pytest.raises(InvalidAmountError):
+ format_decimal_amount(-1, 6)
+
+
+def test_resolve_amount_exactly_one():
+ assert resolve_amount(amount="0.001", amount_atomic=None, decimals=8) == 100_000
+ assert resolve_amount(amount=None, amount_atomic=100_000, decimals=8) == 100_000
+ with pytest.raises(InvalidAmountError, match="exactly one"):
+ resolve_amount(amount=None, amount_atomic=None, decimals=8)
+ with pytest.raises(InvalidAmountError, match="exactly one"):
+ resolve_amount(amount="1", amount_atomic=1, decimals=8)
+ with pytest.raises(InvalidAmountError):
+ resolve_amount(amount=None, amount_atomic=-5, decimals=8)
+ with pytest.raises(InvalidAmountError):
+ resolve_amount(amount=None, amount_atomic="5", decimals=8) # type: ignore[arg-type]
diff --git a/bridge-sdk/tests/test_wait.py b/bridge-sdk/tests/test_wait.py
new file mode 100644
index 0000000..2cc3f70
--- /dev/null
+++ b/bridge-sdk/tests/test_wait.py
@@ -0,0 +1,191 @@
+"""``lifecycle.wait`` — poll ``get_status`` to a caller boundary.
+
+Always stops at ``types.CALLER_BOUNDARIES`` plus any statuses in ``until``, tolerates transient
+read errors (network/RPC hiccups) up to ``max_consecutive_errors`` before giving up, and treats a
+timeout as "still in flight", never as failure.
+"""
+import pytest
+
+from aleo_bridge.checkpoint import FileCheckpointStore
+from aleo_bridge.errors import BridgeError, CheckpointInvalidError, ConfigurationError, PollingTimeoutError
+from aleo_bridge.lifecycle import prepare, wait
+from aleo_bridge.types import Attestation, Progress, Receipt, Status, to_progress
+from tests.fakes.fake_bridge import ALEO_RECIPIENT, SOL_ADDRESS, FakeBridge
+from tests.test_get_status import SIG, _inbound_private
+
+
+def _sol_progress(b):
+ plan = prepare(b.registry, source="aleo/sol", destination="solana/sol", amount="0.000000001", recipient=SOL_ADDRESS)
+ receipt = Receipt(id="at1source", protocol="hyperlane", status=Status.DELIVERY_PENDING, source_tx_id="at1source",
+ protocol_state={"routeId": plan.route_id, "destinationBalanceBeforeAtomic": "100",
+ "expectedDestinationIncreaseAtomic": "1"})
+ return plan, to_progress(plan, receipt)
+
+
+def test_returns_immediately_when_next_is_not_wait_or_status_is_a_boundary():
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ resume = to_progress(plan, progress.receipt.replace(status=Status.SOURCE_SUBMISSION_PENDING))
+ assert wait(b, resume) is not None and wait(b, resume).next == "resume" and b.calls == []
+ done = to_progress(plan, progress.receipt.replace(status=Status.COMPLETED))
+ assert wait(b, done).next == "done" and b.calls == []
+
+
+def test_polls_until_completion_and_reports_only_changes(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ reads = iter([100, 100, 101])
+ b.sol.balance = lambda: next(reads)
+ slept = []
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", slept.append)
+ updates = []
+ out = wait(b, progress, poll_seconds=0, timeout_seconds=10, on_update=updates.append)
+ assert out.next == "done" and out.receipt.status is Status.COMPLETED
+ assert updates == [out] # two unchanged reads produced no update
+ assert slept == [0.0, 0.0]
+
+
+def test_until_adds_stops_and_empty_until_is_an_error(monkeypatch):
+ b = FakeBridge(environment="testnet")
+ plan, payload, message_hash, receipt = _inbound_private(b)
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", lambda s: None)
+ with pytest.raises(ConfigurationError, match="until"):
+ wait(b, to_progress(plan, receipt), until=[])
+ calls = {"n": 0}
+ real_get = b.xreserve.get_attestation
+
+ def flaky(message_hash, *, route=None):
+ calls["n"] += 1
+ return None if calls["n"] == 1 else Attestation(payload, bytes.fromhex(message_hash[2:]), bytes.fromhex(SIG[2:]), "complete")
+ b.xreserve.get_attestation = flaky
+ updates = []
+ out = wait(b, to_progress(plan, receipt), until=[Status.DESTINATION_ACTION_REQUIRED], poll_seconds=0,
+ timeout_seconds=10, on_update=updates.append)
+ assert calls["n"] == 2 and out.next == "complete" and updates == [out]
+ # string statuses are accepted in until
+ calls["n"] = 0
+ assert wait(b, to_progress(plan, receipt), until=["DESTINATION_ACTION_REQUIRED"], poll_seconds=0,
+ timeout_seconds=10).next == "complete"
+
+
+def test_until_rejects_an_unknown_status_name():
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ with pytest.raises(ConfigurationError):
+ wait(b, progress, until=["NOT_A_REAL_STATUS"])
+
+
+def test_timeout_carries_status_and_progress(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ b.sol.balance_lamports = 100
+ clock = iter([0.0, 0.0, 5.0, 11.0])
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.monotonic", lambda: next(clock))
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", lambda s: None)
+ with pytest.raises(PollingTimeoutError, match="DELIVERY_PENDING") as exc:
+ wait(b, progress, poll_seconds=0.05, timeout_seconds=10)
+ assert exc.value.status is Status.DELIVERY_PENDING
+ assert isinstance(exc.value.progress, Progress) and exc.value.progress.next == "wait"
+
+
+def test_poll_floor_and_negative_controls(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ reads = iter([100, 101])
+ b.sol.balance = lambda: next(reads)
+ slept = []
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", slept.append)
+ wait(b, progress, poll_seconds=0.01, timeout_seconds=10)
+ assert slept == [0.1] # floor 0.1 unless exactly 0
+ with pytest.raises(ConfigurationError):
+ wait(b, progress, poll_seconds=-1)
+ with pytest.raises(ConfigurationError):
+ wait(b, progress, timeout_seconds=-1)
+
+
+def test_bound_store_tracks_changes_and_deletes_terminal(tmp_path, monkeypatch):
+ store = FileCheckpointStore(tmp_path)
+ b = FakeBridge(solana=True, checkpoints=store)
+ plan, progress = _sol_progress(b)
+ from aleo_bridge.checkpoint import create_checkpoint
+ store.save(create_checkpoint(plan, progress.receipt, b.registry))
+ b.sol.balance_lamports = 101
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", lambda s: None)
+ assert wait(b, progress, poll_seconds=0, timeout_seconds=10).next == "done"
+ assert store.list() == []
+
+
+# ── transient-error tolerance (controller notes item 1) ───────────────────────
+
+def test_transient_errors_are_retried_with_the_normal_poll_interval(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ slept = []
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", slept.append)
+ calls = {"n": 0}
+
+ def flaky(bridge, plan_, receipt):
+ calls["n"] += 1
+ if calls["n"] <= 3:
+ raise BridgeError("Solana RPC request failed with HTTP status 429")
+ return receipt.replace(status=Status.COMPLETED)
+
+ monkeypatch.setattr("aleo_bridge.lifecycle.get_status", flaky)
+ errors = []
+ out = wait(b, progress, poll_seconds=0, timeout_seconds=10, on_error=errors.append)
+ assert calls["n"] == 4
+ assert out.next == "done" and out.receipt.status is Status.COMPLETED
+ assert len(errors) == 3 and all(isinstance(e, BridgeError) for e in errors)
+ assert slept == [0.0, 0.0, 0.0]
+
+
+def test_six_consecutive_transient_errors_reraise_the_sixth(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", lambda s: None)
+ calls = {"n": 0}
+
+ def always_flaky(bridge, plan_, receipt):
+ calls["n"] += 1
+ raise BridgeError("request failed: connection reset")
+
+ monkeypatch.setattr("aleo_bridge.lifecycle.get_status", always_flaky)
+ with pytest.raises(BridgeError, match="connection reset"):
+ wait(b, progress, poll_seconds=0, timeout_seconds=10)
+ assert calls["n"] == 6 # five tolerated, the sixth re-raises
+
+
+def test_a_flaky_destination_balance_reader_is_retried_not_swallowed(monkeypatch):
+ """Task 6 review item 8, end to end: the branch-6 balance read raises a 429-shaped transport
+ error through the REAL ``get_status``, and ``wait`` retries it until the delivery is visible."""
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ monkeypatch.setattr("aleo_bridge.lifecycle.time.sleep", lambda s: None)
+ calls = {"n": 0}
+
+ def flaky():
+ calls["n"] += 1
+ if calls["n"] <= 2:
+ raise BridgeError("Solana RPC request failed with HTTP status 429")
+ return 101
+
+ b.sol.balance = flaky
+ errors = []
+ out = wait(b, progress, poll_seconds=0, timeout_seconds=10, on_error=errors.append)
+ assert calls["n"] == 3 and len(errors) == 2
+ assert out.next == "done" and out.receipt.status is Status.COMPLETED
+
+
+def test_a_non_transient_error_propagates_on_the_first_attempt(monkeypatch):
+ b = FakeBridge(solana=True)
+ plan, progress = _sol_progress(b)
+ calls = {"n": 0}
+
+ def raises_checkpoint_invalid(bridge, plan_, receipt):
+ calls["n"] += 1
+ raise CheckpointInvalidError("stale checkpoint")
+
+ monkeypatch.setattr("aleo_bridge.lifecycle.get_status", raises_checkpoint_invalid)
+ with pytest.raises(CheckpointInvalidError, match="stale checkpoint"):
+ wait(b, progress, poll_seconds=0, timeout_seconds=10)
+ assert calls["n"] == 1
diff --git a/bridge-sdk/tests/test_xreserve.py b/bridge-sdk/tests/test_xreserve.py
new file mode 100644
index 0000000..5c69967
--- /dev/null
+++ b/bridge-sdk/tests/test_xreserve.py
@@ -0,0 +1,166 @@
+import pytest
+
+from aleo_bridge import encoding as enc
+from aleo_bridge.errors import (AttestationError, BridgeError, ConfigurationError, InvalidAmountError,
+ InvalidRecipientError, UnsupportedRouteError)
+from aleo_bridge.registry import DEFAULT_REGISTRY as REG
+from aleo_bridge.types import Attestation, BurnReceipt, MintReceipt, Status
+from tests.conftest import NULLIFIED_NONCE, USDCX_RECORD
+
+EVM1 = "0x0000000000000000000000000000000000000001"
+RECIPIENT = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n"
+PROOF = "[{ siblings: [0field], leaf_index: 1u32 }, { siblings: [0field], leaf_index: 1u32 }]"
+ONE_LIT = "[" + ",".join(["0u8"] * 31 + ["1u8"]) + "]"
+MAIN = REG.route("xreserve:aleo/usdcx->ethereum/usdc")
+TESTNET = REG.route("xreserve:aleo-testnet/usdcx->sepolia/usdc")
+INBOUND = REG.route("xreserve:ethereum/usdc->aleo/usdcx")
+
+
+def test_routes(bridge):
+ assert bridge.xreserve.outbound_route().id == "xreserve:aleo/usdcx->ethereum/usdc"
+ assert bridge.xreserve.inbound_route().id == "xreserve:ethereum/usdc->aleo/usdcx"
+
+
+def test_private_burn_inputs_match_veil(bridge):
+ program, function, inputs = bridge.xreserve.build_burn_inputs(
+ MAIN, mode="private", amount_atomic=2_500_000, recipient=EVM1, record=USDCX_RECORD, merkle_proof=PROOF)
+ assert (program, function) == ("shielded_usdcx_wrapper.aleo", "private_burn")
+ assert inputs == [USDCX_RECORD, "2500000u128", "0u32", ONE_LIT, PROOF]
+ program, function, inputs = bridge.xreserve.build_burn_inputs(
+ TESTNET, mode="private", amount_atomic=2_500_000, recipient=EVM1, record=USDCX_RECORD, merkle_proof=PROOF)
+ assert program == "shielded_usdcx_wrapper.aleo" and inputs[0] == USDCX_RECORD
+
+
+def test_public_burn_inputs(bridge):
+ assert bridge.xreserve.build_burn_inputs(MAIN, mode="public", amount_atomic=2_500_000, recipient=EVM1, record=None, merkle_proof=None) == \
+ ("usdcx_bridge_v2.aleo", "burn_public", ["2500000u128", "0u32", ONE_LIT])
+ assert bridge.xreserve.build_burn_inputs(MAIN, mode="public-as-signer", amount_atomic=2_500_000, recipient=EVM1, record=None, merkle_proof=None)[1] == "burn_public_as_signer"
+ assert bridge.xreserve.build_burn_inputs(TESTNET, mode="public", amount_atomic=2_500_000, recipient=EVM1, record=None, merkle_proof=None)[0] == "test_usdcx_bridge_v2.aleo"
+
+
+def test_burn_input_validation(bridge):
+ kw = dict(amount_atomic=2_500_000, recipient=EVM1, record=USDCX_RECORD, merkle_proof=PROOF)
+ with pytest.raises(ConfigurationError, match="Unsupported USDCx burn mode"):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="unknown", **kw)
+ with pytest.raises(ConfigurationError, match="private_burn requires a USDCx Token record"):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="private", **{**kw, "record": None})
+ with pytest.raises(ConfigurationError, match=r"\[MerkleProof; 2\]"):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="private", **{**kw, "merkle_proof": "not-a-literal"})
+ with pytest.raises(InvalidAmountError, match="must exceed the 2 USDCx withdrawal fee"):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="public", **{**kw, "amount_atomic": 2_000_000})
+ with pytest.raises(InvalidAmountError, match="greater than zero"):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="public", **{**kw, "amount_atomic": 0})
+ with pytest.raises(InvalidRecipientError):
+ bridge.xreserve.build_burn_inputs(MAIN, mode="public", **{**kw, "recipient": "0x1234"})
+ with pytest.raises(UnsupportedRouteError, match="Aleo-to-Ethereum"):
+ bridge.xreserve.build_burn_inputs(INBOUND, mode="public", **{**kw, "recipient": RECIPIENT})
+
+
+def test_burn_builds_call_and_receipt(bridge):
+ call = bridge.xreserve.burn(EVM1, amount="2.5", mode="private", record=USDCX_RECORD, merkle_proof=PROOF)
+ assert (call.program_id, call.function_name) == ("shielded_usdcx_wrapper.aleo", "private_burn")
+ assert call.inputs == [USDCX_RECORD, "2500000u128", "0u32", ONE_LIT, PROOF]
+ result = call.transact()
+ assert isinstance(result, BurnReceipt)
+ assert (result.transaction_id, result.route_id, result.mode, result.amount_atomic) == ("at1built", MAIN.id, "private", 2_500_000)
+ assert result.receipt.status is Status.SOURCE_CONFIRMING and result.receipt.source_tx_id == "at1built"
+ assert result.receipt.protocol_state == {
+ "routeId": MAIN.id, "burnMode": "private", "amountAtomic": "2500000", "nativeDomain": 0,
+ "nativeRecipientBytes32": "0x" + "00" * 31 + "01", "sourceProgram": "shielded_usdcx_wrapper.aleo",
+ "sourceFunction": "private_burn", "forwardingService": "aleo-burn-attestation"}
+ public = bridge.xreserve.burn(EVM1, amount_atomic=3_000_000, mode="public-as-signer")
+ assert (public.program_id, public.function_name, public.inputs) == ("usdcx_bridge_v2.aleo", "burn_public_as_signer", ["3000000u128", "0u32", ONE_LIT])
+
+
+def test_burn_validates_mode_before_any_amount_or_chain_work(bridge):
+ with pytest.raises(ConfigurationError, match="Unsupported USDCx burn mode"):
+ bridge.xreserve.burn(EVM1, mode="unknown") # no amount= given: an amount check first would raise a different error
+ assert bridge.aleo.record_queries == [] # never reached the record scan
+
+
+def test_burn_rejects_record_or_merkle_proof_for_public_modes(bridge):
+ with pytest.raises(ConfigurationError, match="record=/merkle_proof="):
+ bridge.xreserve.burn(EVM1, amount="2.5", mode="public", record=USDCX_RECORD)
+ with pytest.raises(ConfigurationError, match="record=/merkle_proof="):
+ bridge.xreserve.burn(EVM1, amount="2.5", mode="public-as-signer", merkle_proof="[x]")
+ assert bridge.aleo.record_queries == []
+
+
+def _attested(bridge, nonce="7scalar", recipient=RECIPIENT) -> Attestation:
+ hook = bridge.xreserve.hook_data("private", recipient, nonce)
+ payload = bytes.fromhex("5a2e0acd00000001") + bytes(228) + bytes.fromhex("00000041") + hook
+ return Attestation(payload=payload, message_hash=enc.xreserve_message_hash(payload), attestation=bytes.fromhex("11" * 65), status="complete")
+
+
+def test_hook_data_uses_bridge_environment(bridge):
+ assert bridge.xreserve.hook_data("public", RECIPIENT) == bytes(65)
+ assert bridge.xreserve.hook_data("private", RECIPIENT, "7scalar") == enc.xreserve_hook_data("private", RECIPIENT, "mainnet", "7scalar")
+
+
+def test_private_mint_inputs_match_veil(bridge):
+ att = _attested(bridge)
+ inputs = bridge.xreserve.build_private_mint_inputs(INBOUND, att, RECIPIENT, "7scalar")
+ assert len(inputs) == 5
+ assert inputs[0] == enc.u8_array_literal(att.payload) and inputs[0].count("u8") == 305 and " " not in inputs[0]
+ assert inputs[1] == "[" + ",".join(["17u8"] * 65) + "]"
+ assert inputs[2] == enc.u8_array_literal(att.message_hash) and inputs[2].count("u8") == 32
+ assert inputs[3] == "7scalar" and inputs[4] == RECIPIENT
+
+
+def test_private_mint_rejections(bridge):
+ att = _attested(bridge)
+ with pytest.raises(AttestationError, match="do not match the attested hook"):
+ bridge.xreserve.build_private_mint_inputs(INBOUND, att, RECIPIENT, "8scalar")
+ with pytest.raises(AttestationError, match="do not match the attested hook"):
+ bridge.xreserve.build_private_mint_inputs(INBOUND, att, "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px", "7scalar")
+ with pytest.raises(AttestationError, match="completed Circle attestation"):
+ bridge.xreserve.build_private_mint_inputs(INBOUND, Attestation(att.payload, att.message_hash, att.attestation, "pending"), RECIPIENT, "7scalar")
+ with pytest.raises(AttestationError, match="invalid message hash"):
+ bridge.xreserve.build_private_mint_inputs(INBOUND, Attestation(att.payload, bytes(32), att.attestation, "complete"), RECIPIENT, "7scalar")
+ with pytest.raises(ConfigurationError, match="scalar"):
+ bridge.xreserve.build_private_mint_inputs(INBOUND, att, RECIPIENT, "seven")
+ with pytest.raises(UnsupportedRouteError, match="Ethereum-to-Aleo"):
+ bridge.xreserve.build_private_mint_inputs(MAIN, att, RECIPIENT, "7scalar")
+
+
+def test_private_mint_call_and_receipt(bridge):
+ att = _attested(bridge)
+ call = bridge.xreserve.private_mint(att, RECIPIENT, secret_nonce="7scalar")
+ assert (call.program_id, call.function_name) == ("shielded_usdcx_wrapper.aleo", "private_mint")
+ result = call.delegate(wait=False)
+ assert isinstance(result, MintReceipt) and result.transaction_id == "at1delegated" and result.route_id == INBOUND.id
+ r = result.receipt
+ assert r.id == enc.to_hex(att.message_hash) and r.status is Status.DESTINATION_CONFIRMING and r.destination_tx_id == "at1delegated"
+ assert r.protocol_state["routeId"] == INBOUND.id and r.protocol_state["mintMode"] == "private"
+ assert r.protocol_state["intendedRecipient"] == RECIPIENT and r.protocol_state["nonce"] == "0x" + "00" * 32
+ assert r.protocol_state["destinationProgram"] == "shielded_usdcx_wrapper.aleo" and r.protocol_state["destinationFunction"] == "private_mint"
+ assert "secretNonce" not in r.protocol_state # the secret never travels in receipts
+
+
+def test_get_attestation_uses_route_base_url(bridge):
+ class _Session:
+ def __init__(self): self.urls = []
+ def get(self, url, timeout=None):
+ self.urls.append(url)
+ class R: status_code = 404
+ return R()
+ session = _Session()
+ bridge.xreserve.circle_session = session
+ assert bridge.xreserve.get_attestation("0x" + "22" * 32) is None
+ assert session.urls == ["https://xreserve-api.circle.com/v1/attestations/0x" + "22" * 32]
+ bridge.xreserve.get_attestation(bytes.fromhex("33" * 32), route=REG.route("xreserve:sepolia/usdc->aleo-testnet/usdcx"))
+ assert session.urls[-1].startswith("https://xreserve-api-testnet.circle.com/v1/attestations/0x33")
+
+
+def test_get_attestation_rejects_non_hex_message_hash(bridge):
+ with pytest.raises(BridgeError):
+ bridge.xreserve.get_attestation("nope")
+
+
+def test_is_delivered_reads_bridge_program_nullifier(bridge):
+ assert bridge.xreserve.is_delivered(NULLIFIED_NONCE) is True
+ assert bridge.xreserve.is_delivered("0x" + NULLIFIED_NONCE.hex()) is True
+ assert bridge.xreserve.is_delivered(bytes(32)) is False
+ assert "usdcx_bridge_v2.aleo" in bridge.aleo.fetched
+ with pytest.raises(ConfigurationError, match="32-byte deposit nonce"):
+ bridge.xreserve.is_delivered("0x01")