Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
cab80a6
feat(bridge-sdk): scaffold aleo-bridge-sdk with error taxonomy and ex…
iamalwaysuncomfortable Sep 16, 2026
a4f8a38
feat(bridge-sdk): vendor keccak-256 and base58 primitives
iamalwaysuncomfortable Sep 16, 2026
3d68bb4
feat(bridge-sdk): wire encoders — bech32m, hyperlane limbs, xReserve …
iamalwaysuncomfortable Sep 16, 2026
fb78ab8
feat(bridge-sdk): pinned veil registry (7 chains, 19 assets, 22 route…
iamalwaysuncomfortable Sep 16, 2026
ece5668
feat(bridge-sdk): status enum, plan/receipt/progress and result datac…
iamalwaysuncomfortable Sep 16, 2026
de45ad8
fix(bridge-sdk): derive Progress.error for failed transfers and expor…
iamalwaysuncomfortable Sep 16, 2026
4550d94
feat(bridge-sdk): AleoCall verb ladder with checkpointable delegate a…
iamalwaysuncomfortable Sep 16, 2026
8e12bcc
fix(bridge-sdk): narrow duplicate-broadcast detection and document co…
iamalwaysuncomfortable Sep 16, 2026
c3e18e9
feat(bridge-sdk): Aleo-origin Hyperlane transfer_remote, IGP quote an…
iamalwaysuncomfortable Sep 16, 2026
8180a5b
fix(bridge-sdk): stop test_import_without_optional_extras leaking rei…
iamalwaysuncomfortable Sep 16, 2026
f64ae42
feat(bridge-sdk): xReserve burn and private_mint builders, Circle att…
iamalwaysuncomfortable Sep 16, 2026
b83f625
fix(bridge-sdk): wrap Circle transport and JSON failures in Attestati…
iamalwaysuncomfortable Sep 17, 2026
340e238
feat(bridge-sdk): Sealance freeze-list Merkle tree and exclusion proofs
iamalwaysuncomfortable Sep 17, 2026
ecefb1f
feat(bridge-sdk): shield/unshield for ARC-20 and ARC-22 assets with r…
iamalwaysuncomfortable Sep 17, 2026
eff3201
fix(bridge-sdk): reject mismatched recipient on ARC-20 unshield
iamalwaysuncomfortable Sep 17, 2026
8b3d726
feat(bridge-sdk): Bridge client with from_env/from_profile/status, Al…
iamalwaysuncomfortable Sep 17, 2026
77992bd
fix(bridge-sdk): read the USDCx freeze list from usdcx_freezelist.ale…
iamalwaysuncomfortable Sep 17, 2026
2bfd6b7
fix(bridge-sdk): mapping_value returns None for a missing program
iamalwaysuncomfortable Sep 17, 2026
326175d
test(bridge-sdk): gated read-only live checks (IGP quotes, mailbox an…
iamalwaysuncomfortable Sep 17, 2026
a7a45d3
feat(bridge-sdk): version-1 checkpoint allowlist and FileCheckpointStore
iamalwaysuncomfortable Sep 17, 2026
75052af
feat(bridge-sdk): Ethereum connection with three signer forms and fak…
iamalwaysuncomfortable Sep 17, 2026
41ecd18
feat(bridge-sdk): EvmCall step runner with pre-poll checkpoints, EVM …
iamalwaysuncomfortable Sep 17, 2026
794a7d1
fix(bridge-sdk): AleoCall.__repr__ no longer leaks record/nonce literals
iamalwaysuncomfortable Sep 17, 2026
56fad46
fix(bridge-sdk): Receipt.replace copies its dicts; to_progress raises…
iamalwaysuncomfortable Sep 17, 2026
2d85e45
fix(bridge-sdk): freeze-list root check is fatal when unreadable
iamalwaysuncomfortable Sep 17, 2026
157978c
fix(bridge-sdk): profile creation is exclusive (O_EXCL); home dir mod…
iamalwaysuncomfortable Sep 17, 2026
87b1676
fix(bridge-sdk): xreserve.burn validates mode before touching chain s…
iamalwaysuncomfortable Sep 17, 2026
4fdc2d8
fix(bridge-sdk): triaged minors from the plan-1 review
iamalwaysuncomfortable Sep 17, 2026
6afbadf
test(bridge-sdk): prove eth.py fee arithmetic, legacy gasPrice path, …
iamalwaysuncomfortable Sep 17, 2026
f92e13c
feat(bridge-sdk): eth.quote_transfer_remote with native/collateral fe…
iamalwaysuncomfortable Sep 17, 2026
748f75b
feat(bridge): eth.transfer_remote with approval reset, DispatchId mes…
iamalwaysuncomfortable Sep 17, 2026
0f8acfc
feat(bridge): eth.quote_deposit_usdc with hook data, wrapper-program …
iamalwaysuncomfortable Sep 17, 2026
b672919
fix(bridge-sdk): validate Hyperlane route metadata before contract ca…
iamalwaysuncomfortable Sep 17, 2026
c8687fb
fix(bridge-sdk): validate xReserve route metadata before contract calls
iamalwaysuncomfortable Sep 17, 2026
cfe3100
feat(bridge): eth.deposit_usdc with DepositedToRemote re-verification…
iamalwaysuncomfortable Sep 17, 2026
5729f46
feat(bridge-sdk): eth.source_status for approval and confirming branc…
iamalwaysuncomfortable Sep 17, 2026
075655b
feat(bridge-sdk): eth.recover_source log-scan recovery for Hyperlane …
iamalwaysuncomfortable Sep 17, 2026
f3f8c9d
feat(bridge-sdk): Bridge.eth, bare Web3 wrapping, EVM env vars in fro…
iamalwaysuncomfortable Sep 17, 2026
ed308c6
test(bridge-sdk): live Ethereum read checks and gated Sepolia USDC le…
iamalwaysuncomfortable Sep 17, 2026
c33f2a3
docs(bridge-sdk): document bridge.eth in the README Ethereum section
iamalwaysuncomfortable Sep 17, 2026
1b67563
fix(bridge-sdk): recover_source required semantics, chain-checked sta…
iamalwaysuncomfortable Sep 17, 2026
361ff91
feat(bridge-sdk): sealevel TransferRemote instruction data with veil'…
iamalwaysuncomfortable Sep 17, 2026
58ac424
test(bridge-sdk): leg-1 state files follow the rehearsal convention; …
iamalwaysuncomfortable Sep 17, 2026
1afbb69
feat(bridge-sdk): decode sealevel IGP accounts and quote the lamport …
iamalwaysuncomfortable Sep 17, 2026
4103b93
feat(bridge-sdk): sealevel route metadata, pure PDA derivation, and t…
iamalwaysuncomfortable Sep 17, 2026
6b6553a
feat(bridge-sdk): Solana sync JSON-RPC transport and connection (rpc_…
iamalwaysuncomfortable Sep 17, 2026
68e19e0
fix(bridge-sdk): DispatchId must come from the route's mailbox; xRese…
iamalwaysuncomfortable Sep 17, 2026
87a1adb
fix(bridge-sdk): a checkpoint-store failure after broadcast never los…
iamalwaysuncomfortable Sep 17, 2026
a3d3c5d
feat(bridge-sdk): plan-driven transfer_remote/deposit_usdc and quote …
iamalwaysuncomfortable Sep 17, 2026
a1aa976
fix(bridge-sdk): bounded, chunked and wrapped recovery log scans
iamalwaysuncomfortable Sep 17, 2026
d8bfde1
fix(bridge-sdk): native fee decimals, validated mailbox lookup, match…
iamalwaysuncomfortable Sep 17, 2026
73591c6
docs(bridge-sdk): note BRIDGE_LIVE_ETHEREUM_RPC_URL is an ordinary fr…
iamalwaysuncomfortable Sep 17, 2026
a8ef703
refactor(bridge-sdk): promote _plan_for to _plan.build_plan with a ch…
iamalwaysuncomfortable Sep 17, 2026
4cf603e
feat(bridge-sdk): SolModule quote — IGP payment, network fee, rent an…
iamalwaysuncomfortable Sep 17, 2026
9249dce
feat(bridge-sdk): SolCall — partial sign, fee-payer signature, broadc…
iamalwaysuncomfortable Sep 17, 2026
3a77bc0
feat(bridge-sdk): Solana SOURCE_CONFIRMING status refresh with blockh…
iamalwaysuncomfortable Sep 17, 2026
448351e
feat(bridge-sdk): wire the Solana connection into Bridge (sol propert…
iamalwaysuncomfortable Sep 17, 2026
7a7c2aa
fix(bridge-sdk): SolCall polling — EXPIRED only without a status; log…
iamalwaysuncomfortable Sep 17, 2026
f34e3a1
test(bridge-sdk): gated live Solana reads (IGP decode, leg 11 quote) …
iamalwaysuncomfortable Sep 17, 2026
38fc1db
feat(bridge): pure prepare() planner with veil step lists and route r…
iamalwaysuncomfortable Sep 17, 2026
ebee929
feat(bridge-sdk): quote() dispatch over protocol modules + duck-typed…
iamalwaysuncomfortable Sep 17, 2026
02391c7
fix(bridge-sdk): never lose a broadcast id, and verify the node echoe…
iamalwaysuncomfortable Sep 17, 2026
f6ce2c1
fix(bridge-sdk): a call is single-use once it has broadcast
iamalwaysuncomfortable Sep 17, 2026
6c86e0d
fix(bridge-sdk): a Solana status row with no confirmation level is pr…
iamalwaysuncomfortable Sep 17, 2026
9f67ee3
fix(bridge-sdk): coerce solana= like ethereum=, and derive the status…
iamalwaysuncomfortable Sep 17, 2026
02ebe97
fix(bridge-sdk): close Solana transports properly and never raise fro…
iamalwaysuncomfortable Sep 17, 2026
19797dc
fix(bridge-sdk): keep the key out of tracebacks, re-resolve the route…
iamalwaysuncomfortable Sep 17, 2026
df5d34a
docs(bridge-sdk): document the Solana env aliases; drop two dead decl…
iamalwaysuncomfortable Sep 17, 2026
43d0934
fix(bridge-sdk): name the missing recipient, and validate log_scan_ch…
iamalwaysuncomfortable Sep 17, 2026
8231783
feat(bridge-sdk): execute() with prove->checkpoint->broadcast Aleo le…
iamalwaysuncomfortable Sep 17, 2026
fa8553d
fix(bridge-sdk): arm the single-use guard on an echoed-hash mismatch;…
iamalwaysuncomfortable Sep 17, 2026
5b7e954
feat(bridge-sdk): get_status() branch table incl. nullifier-first inb…
iamalwaysuncomfortable Sep 17, 2026
682a199
fix(bridge-sdk): save-then-delete checkpoints, gate Aleo legs on the …
iamalwaysuncomfortable Sep 18, 2026
f33edd7
feat(bridge-sdk): wait() with caller boundaries, until, transient-err…
iamalwaysuncomfortable Sep 18, 2026
cfc4479
fix(bridge-sdk): stop _message_id aliasing source tx hashes and stop …
iamalwaysuncomfortable Sep 18, 2026
2b46d1c
feat(bridge-sdk): recover() rebuilds progress from checkpoints withou…
iamalwaysuncomfortable Sep 18, 2026
5b5ca47
fix(bridge-sdk): let get_status see a failing destination-balance read
iamalwaysuncomfortable Sep 18, 2026
ec0038a
feat(bridge-sdk): resume() and complete() - idempotent rebroadcast, p…
iamalwaysuncomfortable Sep 18, 2026
cef3230
fix(bridge-sdk): recover() rejects malformed deliveryVerification and…
iamalwaysuncomfortable Sep 18, 2026
7f15242
feat(bridge-sdk): Bridge lifecycle methods, pending(), and additive p…
iamalwaysuncomfortable Sep 18, 2026
644f6de
fix(bridge-sdk): resume requires the checkpointed xReserve hook; comp…
iamalwaysuncomfortable Sep 18, 2026
7e646e4
feat(bridge-sdk): agent tools over the lifecycle verbs with confirm-g…
iamalwaysuncomfortable Sep 18, 2026
d841db4
fix(bridge-sdk): pending() is offline; Bridge.wait forwards the retry…
iamalwaysuncomfortable Sep 18, 2026
a7d69f1
fix(bridge-sdk): agent tools return checkpoints from reads, redact re…
iamalwaysuncomfortable Sep 18, 2026
59a88b4
feat(bridge-sdk): MCP stdio server over the agent tools
iamalwaysuncomfortable Sep 18, 2026
a5ea78e
feat(bridge-sdk): generate AGENTS.md from docstrings; python -m aleo_…
iamalwaysuncomfortable Sep 18, 2026
b87883a
test(bridge-sdk): live harness config/helpers ported from veil, with …
iamalwaysuncomfortable Sep 18, 2026
a2df678
feat(bridge-sdk): live cases + rehearse CLI over the public lifecycle…
iamalwaysuncomfortable Sep 18, 2026
681a107
docs(bridge-sdk): README section for the live tests
iamalwaysuncomfortable Sep 18, 2026
ef6c270
fix(bridge-sdk): veil parity for the live cases — source confirmation…
iamalwaysuncomfortable Sep 18, 2026
33aadd6
test(bridge-sdk): live lifecycle suite over every route, back and forth
iamalwaysuncomfortable Sep 18, 2026
07ed2a2
docs(bridge-sdk): live-test commands, and PyNaCl is a base dependency
iamalwaysuncomfortable Sep 18, 2026
f4361a2
docs(bridge-sdk): rewrite README for the full lifecycle SDK, agent/MC…
iamalwaysuncomfortable Sep 18, 2026
fa05da5
ci: add build-bridge and release-bridge jobs for aleo-bridge-sdk
iamalwaysuncomfortable Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test(bridge-sdk): live lifecycle suite over every route, back and forth
One `live`-marked test per veil case, parametrized from an enumeration of
DEFAULT_REGISTRY rather than a hand-written route list: a route no case covers
raises at import, and a `metadata-required` route is parametrized and skipped
with `registry:metadata-required` instead of disappearing. Plus the testnet
deposit (3 USDC, the amount that clears the 2 USDCx withdrawal fee) and — beyond
veil — the testnet RETURN burn, so "all the routes back and forth" holds on
testnet too.

Every funded test crosses a real process boundary: phase one quotes, prechecks
and calls `execute` once, then returns; that client is dropped and a brand-new
Bridge over the same FileCheckpointStore finishes the transfer through
`pending()` → `recover()` → `wait`/`resume`/`complete`. `execute` is never
called twice for one transfer. Underfunded is a skip with the shortfall (also
when a quote refuses first), a timeout is pending with the resume command.

Harness changes the first real runs forced:

* `cases.run_case(stop_after_execute=...)` — the handover point above.
* Aleo→EVM xReserve delivery is a balance rise, not a drive loop. lifecycle.py
  says Circle exposes no delivery query for that direction and leaves the
  receipt in DELIVERY_PENDING, so `wait` could only ever time out (it did, for
  20 minutes, on a leg whose funds had already landed). veil polls the ERC-20
  balance instead; so do we now, and we recover the withdrawal tx id from the
  Transfer log.
* config: per-environment key/RPC resolution (testnet Aleo key, Sepolia RPC,
  public defaults) and `BRIDGE_LIVE_XRESERVE_AMOUNT`.
* Delivery is asserted as "at least what was quoted", never equality — the
  testnet return delivered 0.996501 USDC against a quoted 0.000001, because the
  live withdrawal fee was 1.0035 USDC, not the registry's 2.
  • Loading branch information
iamalwaysuncomfortable committed Sep 18, 2026
commit 33aadd61c3dfe290a9496919e58c56a78baa955c
100 changes: 93 additions & 7 deletions bridge-sdk/tests/live/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@

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,
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
Expand Down Expand Up @@ -329,11 +332,73 @@ def _select_private_record(bridge: Any, route: Route, amount_atomic: int,
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,
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: 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)
Expand Down Expand Up @@ -394,6 +459,10 @@ def run_case(bridge: Any, case: str, route_id: str, *, state_path: Path | str, r
# 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")

Expand All @@ -404,6 +473,23 @@ def run_case(bridge: Any, case: str, route_id: str, *, state_path: Path | str, r
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)
Expand Down Expand Up @@ -462,8 +548,8 @@ def run(bridge: Any, route_id: str, *, state_path: Path | str, recipient: str |

__all__ = [
"CASES", "CASE_NAMES", "CASE_TIMEOUT_SECONDS", "CaseSpec", "LiveCaseError", "RUNNERS", "Underfunded",
"asset_ref", "case_for_route", "default_amount", "default_recipient", "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",
"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",
]
86 changes: 84 additions & 2 deletions bridge-sdk/tests/live/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@

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",
Expand Down Expand Up @@ -123,6 +148,60 @@ def case_route_override(case: str, env: Mapping[str, str] | None = None) -> str
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_<CASE>_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:
Expand All @@ -133,9 +212,12 @@ def recipient_override(family: str, env: Mapping[str, str] | None = None) -> str


__all__ = [
"CASE_NAMES", "ENVIRONMENTS", "FUNDS_VAR", "LiveConfigError", "MAINNET_ACK", "MAINNET_ACK_VAR",
"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",
"case_route_override", "live_funds_enabled", "live_state_path", "mainnet_case_enabled",
"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",
]
Loading