Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions bridge-sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ back are dict entries instead — ``{"next": "failed", "error", "error_type"}``
when no store is bound. Finish any entry with ``recover`` → ``wait`` / ``resume`` / ``complete``,
never by starting a new transfer.

### `quote(self, *, source_chain: 'str | None' = None, source_asset: 'str | None' = None, destination_chain: 'str | None' = None, destination_asset: 'str | None' = None, bridge_protocol: 'str | None' = None, route=None, amount=None, amount_atomic=None, recipient: 'str', sender: 'str | None' = None, mint_mode: 'str' = 'public', secret_nonce: 'str' = '0scalar', cctp=None)`
### `quote(self, *, source_chain: 'str | None' = None, source_asset: 'str | None' = None, destination_chain: 'str | None' = None, destination_asset: 'str | None' = None, bridge_protocol: 'str | None' = None, route=None, amount=None, amount_atomic=None, recipient: 'str', sender: 'str | None' = None, mint_mode: 'str' = 'public', secret_nonce: 'str | None' = None, cctp=None)`

Price a transfer and get the plan that ``execute`` takes. Nothing is signed.

Expand All @@ -73,7 +73,10 @@ only when more than one route fits — or ``route=`` with a ``Route`` from
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
yourself with ``complete``. For a private mint to the bridge's own
account the nonce is derived from the view key and kept in the bound
identity store, so no ``secret_nonce`` is needed; pass your own to
override it (then keep it). 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
Expand Down Expand Up @@ -127,14 +130,18 @@ 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. A private
xReserve deposit for the bridge's own account finds its nonce in the bound
identity store; pass ``secret_nonce`` only if the deposit was made with one.

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

Claim a private USDCx or native-USDC CCTP destination mint.

Requires the same ``secret_nonce`` given to ``execute``; the SDK never
stored it. Submits exactly one ``private_mint`` and returns
A private USDCx mint to the bridge's own account needs no ``secret_nonce``:
the scalar comes from the bound identity store, re-derived from the view
key if the store lost it. A deposit made with a caller-supplied nonce
needs that same nonce back. Submits exactly one ``private_mint`` and returns
``DESTINATION_CONFIRMING`` progress to ``wait`` on. CCTP needs no secret
nonce, but requires a destination signer and gas. Set ``manual_mint=True``
to explicitly authorize fallback for stalled forwarding; an already
Expand Down Expand Up @@ -216,11 +223,22 @@ 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.
Ethereum, and `complete` must reveal the same nonce. When the recipient IS
the bridge's own Aleo address and a journal directory is bound (the
profile's, or `BRIDGE_CHECKPOINT_DIR`), the bridge derives the nonce from
the account's view key the way the Shield wallet does, keeps it in
`.private-mint-identities.json` beside the journal, and re-derives it if
that file is lost — no `secret_nonce` is needed anywhere. Its `.counter`
companion is what keeps each deposit on a fresh nonce: if BOTH files are
lost, tell the user to set the counter above the last one used before
depositing again, or a later deposit can be linked to an earlier one. A
deposit made under `eth.deposit_usdc`'s Tier-2 `0scalar` default still
completes without a nonce. A user-supplied
`secret_nonce` overrides that: the SDK **never stores** it and checkpoints
exclude it (and every other secret), so tell the user to keep it for
`complete` (the default `0scalar` adds no entropy). A deposit to any other
recipient needs a supplied nonce — only the recipient's Aleo key can
complete a private mint, so confirm the recipient 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.
Expand Down Expand Up @@ -290,10 +308,15 @@ from the published list and cached until the list changes. Pass one
must exceed 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]'`
### `xreserve.private_mint(self, attestation: 'Attestation', recipient: 'str', *, secret_nonce: 'str | None' = None, route: 'Route | None' = None) -> 'AleoCall[MintReceipt]'`

Finish a private-mode deposit: the only user-signed Aleo step of the inbound flow (``wrapper.private_mint``).

An explicit ``secret_nonce`` is used as given. Without one, the bridge resolves the scalar
for its own account from the identity store that reserved the deposit (re-deriving it from
the view key when the store lacks it); a deposit made with a caller-supplied nonce needs
that nonce back.

### `xreserve.get_attestation(self, message_hash: "'str | bytes'", *, route: 'Route | None' = None) -> 'Attestation | None'`

One Circle request for *message_hash*; ``None`` while pending (404).
Expand Down
96 changes: 62 additions & 34 deletions bridge-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ if progress.next == "resume":
```

A Private Bridge USDCx deposit needs the recipient to claim the funds with
their Aleo account and original secret nonce. The [USDC Bridging Guide](#usdc-bridging-guide)
explains that choice and the `complete` call used to claim.
their Aleo account; the bridge derives and keeps the claim nonce for its own
account. The [USDC Bridging Guide](#usdc-bridging-guide) explains that choice
and the `complete` call used to claim.

## Recover Funds

Expand Down Expand Up @@ -336,12 +337,14 @@ funds. It does not authorize those actions. Follow `progress.next`:
| --- | --- |
| `wait` | Call `bridge.wait(progress)` to continue monitoring. |
| `resume` | Call `bridge.resume(progress)` to submit the unfinished source operation. |
| `complete` | Call `bridge.complete(progress, secret_nonce=secret_nonce)` to mint private USDCx on Aleo. |
| `complete` | Call `bridge.complete(progress)` to mint private USDCx on Aleo (add `secret_nonce=` only if you supplied one at deposit). |
| `done` | Delivery is complete; no further action is needed. |
| `failed` | Inspect `progress.error` before deciding what to do. |

`resume` and `complete` can submit transactions. A private USDCx mint requires
the original secret nonce, which must be stored separately from the journal.
`resume` and `complete` can submit transactions. A private USDCx mint deposited
under a nonce you supplied requires that same nonce, which must be stored
separately from the journal; a derived nonce is found again from the identities
file beside the journal or re-derived from the account's view key.
**Do not call `execute` again after an ambiguous submission.** A timeout or lost
RPC response does not establish that the original transaction failed.

Expand Down Expand Up @@ -429,9 +432,10 @@ above supplies the same transfer details that a journal would have retained.
Other routes may need additional information:

- **Private USDCx deposits:** recover the original commitment data from the
Ethereum deposit and retain the original secret nonce for the claim. A lost
nonce cannot be reconstructed from public chain data; the recipient cannot
complete the claim through this flow without it.
Ethereum deposit. A nonce the bridge derived is re-derived from the account's
view key at claim time. A nonce the application supplied cannot be
reconstructed from public chain data; the recipient cannot complete the
claim through this flow without it.
- **Aleo-origin transfers:** recovery can track source confirmation, but some
delivery checks depend on the destination balance recorded before submission.
Without that information, confirm receipt on the destination chain; the SDK
Expand All @@ -444,9 +448,11 @@ The SDK does not yet reconstruct every route from a transaction hash alone.
The example above applies specifically to confirmed Ethereum-to-Aleo Hyperlane
transfers; do not reuse its recovery data for a different route.

Keep any private-mint nonce separately: restoring a checkpoint cannot replace
that claim secret. Checkpoints exclude private keys and private record
plaintext, but can include an authorized Aleo transaction awaiting broadcast.
Keep any private-mint nonce you supplied yourself separately: restoring a
checkpoint cannot replace that claim secret. Derived nonces sit beside the
journal in `.private-mint-identities.json`; back it up with the journal.
Checkpoints exclude private keys and private record plaintext, but can include
an authorized Aleo transaction awaiting broadcast.
Protect the journal accordingly. Applications with existing storage can save
checkpoints from `on_checkpoint` using `checkpoint.to_json()`.

Expand Down Expand Up @@ -535,33 +541,22 @@ Private Bridge conceals the recipient's Aleo address in the Ethereum deposit
and delivers funds as a private balance. Unlike automatic delivery, it requires
the recipient to claim the funds before spending them.

The flow has three stages: retain a secret nonce, submit the deposit using
that nonce, and claim the USDCx when it is ready. The nonce is a random value
used to conceal the address; the claim requires both the original nonce and
the recipient's Aleo account.

**Save the nonce before sending funds and retain it until the claim completes.**
Losing it prevents the recipient from completing the claim through this flow.
The bridge journal does not save it, so restoring the journal alone is not
enough. The default `0scalar` provides no secrecy; use a securely generated
nonce for this option.

This is an alternative to the automatic delivery example above. Store the
nonce as an Aleo scalar literal in `BRIDGE_MINT_SECRET_NONCE`. The example uses
the configured Aleo account as the recipient, so that account can claim the
funds later. Request a quote with `mint_mode="private"`:
The deposit commits the recipient to a secret nonce, and the claim reveals that
nonce. When the recipient is the bridge's own Aleo account and the client keeps
a journal (`Bridge.from_profile()`, or `checkpoints=FileCheckpointStore(...)`),
the bridge derives the nonce from the account's view key the way the Shield
wallet does, keeps it beside the journal, and supplies it again at claim time.
Nothing needs to be saved by hand. Request a quote with `mint_mode="private"`:

```python
secret_nonce = os.environ["BRIDGE_MINT_SECRET_NONCE"] # Aleo scalar: decimal digits + "scalar".
private_quote = bridge.quote(
source_chain="ethereum",
source_asset="usdc",
destination_chain="aleo",
destination_asset="usdcx",
amount="2", # 2 USDC, not 2 atomic units.
recipient=bridge.aleo_address(),
recipient=bridge.aleo_address(), # The bridge's own account: the nonce is derived for it.
mint_mode="private",
secret_nonce=secret_nonce,
)
print(private_quote.fees) # Review Ethereum fees before submitting.
print(private_quote.amount_out) # Expected USDCx amount in display units.
Expand All @@ -573,19 +568,52 @@ ready. Claiming requires the recipient to authorize an Aleo transaction and
can incur an Aleo fee:

```python
private_progress = bridge.execute(private_quote.plan, secret_nonce=secret_nonce)
private_progress = bridge.execute(private_quote.plan)
private_progress = bridge.wait(private_progress)

if private_progress.next == "complete": # The deposit is ready for the recipient to claim.
private_progress = bridge.complete(private_progress, secret_nonce=secret_nonce)
private_progress = bridge.complete(private_progress)
private_progress = bridge.wait(private_progress)

print(private_progress.next, private_progress.error) # done None after the claim confirms.
```

Derived nonces live in `.private-mint-identities.json` inside the journal
directory, with a `.counter` file beside it. Treat both like a view key: the
file holds the secrets that open the deposits, so protect it and back it up
with the journal. Losing the identities file does not lose the funds. The claim
re-derives the nonce from the view key by counting up, then saves it again. The
`.counter` file is what keeps every deposit on a fresh nonce; if it is lost too,
set it to a number above the last counter you used before depositing again,
otherwise a later deposit can reuse a nonce and be linked to an earlier one.
Pass `private_mint_identities=` to `Bridge(...)` to keep the identities
elsewhere.

#### Bring your own nonce

Pass `secret_nonce=` to deposit under a nonce the application manages. This is
required when the recipient is not the bridge's own account, since only the
recipient's wallet should hold its secrets, and useful when an application keeps
its own secret store. The same value must come back on `execute`, `resume` and
`complete`; the bridge never stores a supplied nonce and the journal excludes
it. Generate it with `str(aleo.mainnet.Scalar.random())` and save it before
sending funds. The default `0scalar` provides no secrecy.

```python
secret_nonce = os.environ["BRIDGE_MINT_SECRET_NONCE"] # Aleo scalar: decimal digits + "scalar".
private_quote = bridge.quote(
source_chain="ethereum", source_asset="usdc",
destination_chain="aleo", destination_asset="usdcx",
amount="2", recipient=recipient, mint_mode="private", secret_nonce=secret_nonce,
)
private_progress = bridge.wait(bridge.execute(private_quote.plan, secret_nonce=secret_nonce))
if private_progress.next == "complete":
private_progress = bridge.wait(bridge.complete(private_progress, secret_nonce=secret_nonce))
```

If the application closes or monitoring times out, the deposit may still be
in progress. Recover the existing transfer rather than sending USDC again.
Keep the same nonce for any remaining deposit or claim step; see
in progress. Recover the existing transfer rather than sending USDC again; a
supplied nonce must be given again for any remaining deposit or claim step. See
[Recover Funds](#recover-funds).

## Shielding Assets
Expand Down Expand Up @@ -921,7 +949,7 @@ does not prove delivery: the matching message event and exact USDC mint are requ

For Arc → Aleo, select `source_chain="arc"`, `source_asset="usdc"`, and
`destination_chain="aleo"`; public, record, and private mint modes work as on
Ethereum. Private mint completion still requires the original secret nonce.
Ethereum, including the derived claim nonce for the bridge's own account.
Aleo → Arc burns use domain 26, enforce a two-USDCx minimum, and fetch a live
withdrawal fee estimate again before proving. Provider errors are surfaced instead
of presenting a static estimate as current. Public outbound xReserve delivery is
Expand Down
21 changes: 16 additions & 5 deletions bridge-sdk/codegen/gen_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,22 @@
### 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.
Ethereum, and `complete` must reveal the same nonce. When the recipient IS
the bridge's own Aleo address and a journal directory is bound (the
profile's, or `BRIDGE_CHECKPOINT_DIR`), the bridge derives the nonce from
the account's view key the way the Shield wallet does, keeps it in
`.private-mint-identities.json` beside the journal, and re-derives it if
that file is lost — no `secret_nonce` is needed anywhere. Its `.counter`
companion is what keeps each deposit on a fresh nonce: if BOTH files are
lost, tell the user to set the counter above the last one used before
depositing again, or a later deposit can be linked to an earlier one. A
deposit made under `eth.deposit_usdc`'s Tier-2 `0scalar` default still
completes without a nonce. A user-supplied
`secret_nonce` overrides that: the SDK **never stores** it and checkpoints
exclude it (and every other secret), so tell the user to keep it for
`complete` (the default `0scalar` adds no entropy). A deposit to any other
recipient needs a supplied nonce — only the recipient's Aleo key can
complete a private mint, so confirm the recipient 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.
Expand Down
Loading
Loading