Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions bridge-sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Everything from the environment (spec §3.3); writes nothing to disk. Overrides:
### `from_profile(home: 'Any' = None, *, network: 'str | None' = None, endpoint: 'str | None' = None, ethereum: 'Any' = None, solana: 'Any' = None, evm: '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``.
creating. Side-chain connections come from the arguments or the same env variables as ``from_env``;
a chain neither names falls back to a read-only connection over the profile's ``rpc_urls``.

### `status(self) -> 'BridgeStatus'`

Expand Down Expand Up @@ -77,7 +78,9 @@ 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``.
user fees + amount before ``execute``. Refuses with ``RoutePausedError`` /
``PauseCheckError`` when a pause switch on either end reads paused or
cannot be read (see ``pause_status``).

### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, compliance_proofs: 'ComplianceProofs' = 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)`

Expand All @@ -97,8 +100,10 @@ record and computes the freeze-list exclusion proof, caching it until
the list changes; ``merkle_proof`` is the earlier name for one literal
bundle). 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.
pinned. Refuses with ``RoutePausedError`` / ``PauseCheckError`` when a
pause switch on either end reads paused or cannot be read. 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)`

Expand Down Expand Up @@ -127,7 +132,9 @@ 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.
missing deposit/dispatch. Never repeats a confirmed step. Refuses with
``RoutePausedError`` / ``PauseCheckError`` when a pause switch on either
end reads paused or cannot be read.

### `complete(self, progress, *, secret_nonce: 'str | None' = None, on_checkpoint=None, proving: 'str' = 'delegate', manual_mint: 'bool' = False)`

Expand All @@ -139,6 +146,8 @@ stored it. Submits exactly one ``private_mint`` and returns
nonce, but requires a destination signer and gas. Set ``manual_mint=True``
to explicitly authorize fallback for stalled forwarding; an already
submitted destination transaction is observed rather than repeated.
Refuses with ``RoutePausedError`` / ``PauseCheckError`` when a pause switch
on the destination side reads paused or cannot be read.

### `pending(self) -> 'list'`

Expand All @@ -154,6 +163,19 @@ could not even read back come back as ``{"next": "failed", "error", "error_type"
(naming the ``checkpoint_id`` or the ``path``) alongside the healthy ``Progress`` objects,
so a stale or corrupt file can never make a transfer that is still on the wire invisible.

### `pause_status(self, route, *, sides=('source', 'destination')) -> 'PauseStatus'`

Read every operator pause switch governing ``route`` (a ``Route``, its id, or a ``Plan``), live.

Both ends are read unless ``sides`` narrows it: the ``pause`` mapping and enrolled router of the
Aleo warp program (plus the ARC-20 token's ``pause__``), the USDCx bridge and stablecoin pauses,
``paused()`` on xReserve and the CCTP transmitter, any ``PausableHook`` / ``PausableIsm`` in the
Ethereum Mailbox's trees, and the Solana token PDA's enrolled router. Nothing is cached and the
result never raises for a pause: ``status.paused`` / ``status.unknown`` say so and
``status.reason`` names the switch. ``quote``, ``execute``, ``resume`` and ``complete`` run this same
check and refuse with ``RoutePausedError`` or ``PauseCheckError``. A far-side EVM chain with no
configured connection is read over the profile's ``rpc_urls`` or the registry's public endpoints.

## Serving a chatting user (the conversation pattern)

### Keys and identity
Expand Down
26 changes: 26 additions & 0 deletions bridge-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,32 @@ Keep `quote.plan` unchanged when accepting the quote so the submitted transfer
uses the reviewed asset, amount, and recipient. Request a new quote to change
those details.

### Paused routes

Every bridge deployment has an operator switch that can halt it: the `pause`
mapping on the Aleo warp and USDCx programs, `paused()` on Circle's xReserve
and CCTP transmitters, a pausable hook inside the Ethereum Hyperlane Mailbox,
and the enrolled-router table of the Solana warp program. `quote`, `execute`,
`resume` and `complete` read every switch that governs the route, on both
ends, right before they act, and refuse with `RoutePausedError` naming the
switch when one is set. A switch that cannot be read after retries refuses
with `PauseCheckError`; the SDK never proceeds on a guess.

Ask before quoting with `bridge.pause_status(route)`, which returns each
switch and its value:

```python
status = bridge.pause_status("hyperlane:aleo/eth->ethereum/eth")
print(status.paused, status.reason)
for switch in status.switches:
print(switch.chain_id, switch.side, switch.locator, switch.switch, switch.state)
```

Reading the far side of an Aleo-origin route needs an RPC for that chain:
the one you configured, one saved with `profile.set_rpc_url("ethereum", url)`,
or the public read endpoint the registry pins. Those public endpoints are used
for pause reads only, never for signing.

### 2. Submit a bridge transaction from the source chain

The source transaction deposits the sender's funds into the bridge so they
Expand Down
2 changes: 1 addition & 1 deletion bridge-sdk/codegen/gen_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
OUTS = [_ROOT / "AGENTS.md", _ROOT / "python" / "aleo_bridge" / "AGENTS.md"]

TIER1 = ["from_env", "from_profile", "status", "quote", "execute", "wait", "recover", "resume",
"complete", "pending"]
"complete", "pending", "pause_status"]
TIER2 = [
("hyperlane.transfer_remote", HyperlaneModule.transfer_remote),
("hyperlane.quote_gas_payment", HyperlaneModule.quote_gas_payment),
Expand Down
32 changes: 27 additions & 5 deletions bridge-sdk/python/aleo_bridge/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Everything from the environment (spec §3.3); writes nothing to disk. Overrides:
### `from_profile(home: 'Any' = None, *, network: 'str | None' = None, endpoint: 'str | None' = None, ethereum: 'Any' = None, solana: 'Any' = None, evm: '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``.
creating. Side-chain connections come from the arguments or the same env variables as ``from_env``;
a chain neither names falls back to a read-only connection over the profile's ``rpc_urls``.

### `status(self) -> 'BridgeStatus'`

Expand Down Expand Up @@ -77,7 +78,9 @@ 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``.
user fees + amount before ``execute``. Refuses with ``RoutePausedError`` /
``PauseCheckError`` when a pause switch on either end reads paused or
cannot be read (see ``pause_status``).

### `execute(self, plan, *, on_checkpoint=None, proving: 'str' = 'delegate', mode: 'str | None' = None, record: 'str | None' = None, compliance_proofs: 'ComplianceProofs' = 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)`

Expand All @@ -97,8 +100,10 @@ record and computes the freeze-list exclusion proof, caching it until
the list changes; ``merkle_proof`` is the earlier name for one literal
bundle). 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.
pinned. Refuses with ``RoutePausedError`` / ``PauseCheckError`` when a
pause switch on either end reads paused or cannot be read. 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)`

Expand Down Expand Up @@ -127,7 +132,9 @@ 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.
missing deposit/dispatch. Never repeats a confirmed step. Refuses with
``RoutePausedError`` / ``PauseCheckError`` when a pause switch on either
end reads paused or cannot be read.

### `complete(self, progress, *, secret_nonce: 'str | None' = None, on_checkpoint=None, proving: 'str' = 'delegate', manual_mint: 'bool' = False)`

Expand All @@ -139,6 +146,8 @@ stored it. Submits exactly one ``private_mint`` and returns
nonce, but requires a destination signer and gas. Set ``manual_mint=True``
to explicitly authorize fallback for stalled forwarding; an already
submitted destination transaction is observed rather than repeated.
Refuses with ``RoutePausedError`` / ``PauseCheckError`` when a pause switch
on the destination side reads paused or cannot be read.

### `pending(self) -> 'list'`

Expand All @@ -154,6 +163,19 @@ could not even read back come back as ``{"next": "failed", "error", "error_type"
(naming the ``checkpoint_id`` or the ``path``) alongside the healthy ``Progress`` objects,
so a stale or corrupt file can never make a transfer that is still on the wire invisible.

### `pause_status(self, route, *, sides=('source', 'destination')) -> 'PauseStatus'`

Read every operator pause switch governing ``route`` (a ``Route``, its id, or a ``Plan``), live.

Both ends are read unless ``sides`` narrows it: the ``pause`` mapping and enrolled router of the
Aleo warp program (plus the ARC-20 token's ``pause__``), the USDCx bridge and stablecoin pauses,
``paused()`` on xReserve and the CCTP transmitter, any ``PausableHook`` / ``PausableIsm`` in the
Ethereum Mailbox's trees, and the Solana token PDA's enrolled router. Nothing is cached and the
result never raises for a pause: ``status.paused`` / ``status.unknown`` say so and
``status.reason`` names the switch. ``quote``, ``execute``, ``resume`` and ``complete`` run this same
check and refuse with ``RoutePausedError`` or ``PauseCheckError``. A far-side EVM chain with no
configured connection is read over the profile's ``rpc_urls`` or the registry's public endpoints.

## Serving a chatting user (the conversation pattern)

### Keys and identity
Expand Down
18 changes: 9 additions & 9 deletions bridge-sdk/python/aleo_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ def _require_aleo_sdk() -> None:
from .errors import ( # noqa: E402
AmbiguousRouteError, AttestationError, BridgeError, ChainMismatchError, CheckpointInvalidError,
ConfigurationError, DeliveryUnknownError, InsufficientBalanceError, InvalidAmountError,
InvalidRecipientError, MissingExtraError, NotResumableError, PollingTimeoutError,
RegistryVersionMismatchError, RouteNotFoundError, RouteUnavailableError, UnsupportedRouteError,
InvalidRecipientError, MissingExtraError, NotResumableError, PauseCheckError, PollingTimeoutError,
RegistryVersionMismatchError, RouteNotFoundError, RoutePausedError, RouteUnavailableError, UnsupportedRouteError,
)
from .registry import DEFAULT_REGISTRY, Asset, Chain, Locator, Privacy, Registry, Route, validate_registry # noqa: E402
from .types import ( # noqa: E402
CctpOptions, EvmCctpQuote, 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,
MintReceipt, PauseStatus, PauseSwitch, Plan, PreparedTx, PrivacyReceipt, Progress, Quote, Receipt,
SolanaHyperlaneQuote, Status, Step, to_progress,
)
from ._calls import AleoCall, EvmCall, SolCall # noqa: E402
from .checkpoint import (Checkpoint, CheckpointLoadResult, CheckpointProblem, CheckpointStore, FileCheckpointStore, # noqa: E402
Expand All @@ -66,14 +66,14 @@ def _require_aleo_sdk() -> None:
__all__ = [
"__version__", "AmbiguousRouteError", "AttestationError", "BridgeError", "ChainMismatchError",
"CheckpointInvalidError", "ConfigurationError", "DeliveryUnknownError", "InsufficientBalanceError",
"InvalidAmountError", "InvalidRecipientError", "MissingExtraError", "NotResumableError",
"PollingTimeoutError", "RegistryVersionMismatchError", "RouteNotFoundError", "RouteUnavailableError",
"UnsupportedRouteError",
"InvalidAmountError", "InvalidRecipientError", "MissingExtraError", "NotResumableError", "PauseCheckError",
"PollingTimeoutError", "RegistryVersionMismatchError", "RouteNotFoundError", "RoutePausedError",
"RouteUnavailableError", "UnsupportedRouteError",
"Asset", "Chain", "DEFAULT_REGISTRY", "Locator", "Privacy", "Registry", "Route", "validate_registry",
"CctpOptions", "EvmCctpQuote", "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",
"Fee", "GasQuote", "MintReceipt", "PauseStatus", "PauseSwitch", "Plan", "PreparedTx", "PrivacyReceipt",
"Progress", "Quote", "Receipt", "SolanaHyperlaneQuote", "Status", "Step", "to_progress",
"AleoCall", "Bridge", "CircleClient", "DEFAULT_ENDPOINT", "EMPTY_MERKLE_PROOF_PAIR", "FreezeList",
"ComplianceProof", "ComplianceProofs", "MerkleProofInput", "MerkleProofPair",
"HyperlaneModule", "PrivacyModule", "Profile", "XReserveModule",
Expand Down
24 changes: 23 additions & 1 deletion bridge-sdk/python/aleo_bridge/_evm_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,26 @@
"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"]
def _view(name: str, inputs: list[tuple[str, str]], outputs: list[tuple[str, str]]) -> dict:
return {"type": "function", "name": name, "stateMutability": "view",
"inputs": [{"name": n, "type": t} for n, t in inputs],
"outputs": [{"name": n, "type": t} for n, t in outputs]}


# Pause switches and the Hyperlane Mailbox hook / ISM tree getters (OpenZeppelin Pausable, Hyperlane
# IPostDispatchHook / IInterchainSecurityModule, DomainRoutingHook, StaticAggregationHook/Ism, DomainRoutingIsm).
PAUSE_ABI = [
_view("paused", [], [("", "bool")]),
_view("hookType", [], [("", "uint8")]),
_view("moduleType", [], [("", "uint8")]),
_view("defaultHook", [], [("", "address")]),
_view("requiredHook", [], [("", "address")]),
_view("defaultIsm", [], [("", "address")]),
_view("hooks", [("destination", "uint32")], [("", "address")]),
_view("hooks", [("message", "bytes")], [("", "address[]")]),
_view("modulesAndThreshold", [("message", "bytes")], [("modules", "address[]"), ("threshold", "uint8")]),
_view("module", [("origin", "uint32")], [("", "address")]),
]

__all__ = ["ERC20_ABI", "EVM_CHAIN_BY_ENVIRONMENT", "MAILBOX_ABI", "PAUSE_ABI", "WARP_ROUTE_ABI", "XRESERVE_ABI",
"ZERO_ADDRESS"]
12 changes: 12 additions & 0 deletions bridge-sdk/python/aleo_bridge/_registry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@
"protocolDomains": {"hyperlane": 11155111, "xreserve": 0}},
]

# Public JSON-RPC endpoints used ONLY to read pause switches on a chain the caller configured no
# connection for (never for signing, balances or anything else). Kept off the Chain entries so the
# legacy route fingerprints in _registry_compatibility stay byte-identical.
PUBLIC_READ_RPC_URLS = {
"ethereum": ["https://ethereum-rpc.publicnode.com", "https://1rpc.io/eth"],
"arc": ["https://rpc.mainnet.arc.io"],
"base": ["https://base-rpc.publicnode.com", "https://1rpc.io/base"],
"arbitrum": ["https://arbitrum-one-rpc.publicnode.com", "https://1rpc.io/arb"],
"hyperevm": ["https://rpc.hyperliquid.xyz/evm"],
"sepolia": ["https://ethereum-sepolia-rpc.publicnode.com", "https://1rpc.io/sepolia"],
}

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},
Expand Down
27 changes: 27 additions & 0 deletions bridge-sdk/python/aleo_bridge/_sealevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,33 @@ def pubkey(self) -> str:
return b58encode(self.take(32))


def decode_hyperlane_token_remote_routers(data: bytes) -> dict[int, bytes]:
"""``remote_routers`` of a warp program's ``HyperlaneToken<T>`` account (hyperlane-monorepo 45c0988).

Layout, Borsh little-endian, as ``AccountData<HyperlaneToken<T>>``: u8 initialized; u8 bump; 32 mailbox;
32 mailbox_process_authority; u8 dispatch_authority_bump; u8 decimals; u8 remote_decimals;
Option<Pubkey> owner; Option<Pubkey> interchain_security_module; Option<(Pubkey, IgpType)>
interchain_gas_paymaster (enum tag u8 + Pubkey); HashMap<u32, u64> destination_gas; HashMap<u32, H256>
remote_routers. ``plugin_data`` and ``fee_config`` follow and are not decoded. A missing domain means the
operator un-enrolled that route — the Sealevel program has no pause flag of its own.
"""
try:
cursor = _Cursor(data)
if cursor.u8() != 1:
raise ConfigurationError("malformed Sealevel token account data: account is not initialized")
cursor.take(1 + 32 + 32 + 1 + 1 + 1) # bump, mailbox, process authority, bump, decimals x2
for _ in range(2): # owner, ISM
if cursor.u8() == 1:
cursor.take(32)
if cursor.u8() == 1: # IGP: (Pubkey, enum tag, Pubkey)
cursor.take(32 + 1 + 32)
for _ in range(cursor.u32()): # destination_gas: u32 -> u64
cursor.take(4 + 8)
return {cursor.u32(): cursor.take(32) for _ in range(cursor.u32())}
except ConfigurationError as exc:
raise ConfigurationError(f"malformed Sealevel token account data: {exc}") from exc


def decode_igp_account(data: bytes) -> IgpAccount:
"""Decode the terminal ``Igp`` account (the ``inner`` of an OverheadIgp), SEALEVEL_NOTES §4."""
cursor = _Cursor(data)
Expand Down
Loading
Loading