Skip to content

feat: USDCx outbound — burn + release tracking - #376

Draft
sadiq1971 wants to merge 3 commits into
feat/bridge-apifrom
feat/usdcx-outbound
Draft

feat: USDCx outbound — burn + release tracking#376
sadiq1971 wants to merge 3 commits into
feat/bridge-apifrom
feat/usdcx-outbound

Conversation

@sadiq1971

Copy link
Copy Markdown
Member

Part of #359 and epic #361. Stacked on #375 (bridge API) — review the last commit only until #375 merges.

What

USDCx outbound: burn on Canton via BridgeUserAgreement_Burn, Circle releases USDC on Ethereum, the relayer tracks the release. Closes the loop — with this PR the full USDCx lifecycle (deposit quote → auto-mint tracking → withdraw → release tracking) is wired end to end.

Token SDK (pkg/cantonsdk/token)

  • PrepareBurn / BurnByPartyID exercise BridgeUserAgreement_Burn on the user's own agreement contract (a user-party choice — never operator-signed), reusing the existing machinery end to end: agreement discovery via ACS, holdings selection via the transfer path's UTXO picker, choice context + disclosed contracts via the AnyValue converters, external signing via prepareInstructionTx/ExecuteTransfer, custodial signing via exerciseInstructionAsCustodial.
  • RegistryClient.GetBurnMintFactory — burn-mint factory discovery; response envelope is identical to the transfer factory, so both now share one parser (postFactoryEndpoint).
  • Gated by new burn_mint_package_id / burn_mint_module token-client config (values come from the utility-bridge DAR).

Bridge API (pkg/bridgeapi)

  • POST /api/v2/bridge/withdraw/prepare + /execute (external keys, incl. the fingerprint-vs-signed_by check from the transfer flow; prepared burns are single-use) and /withdraw/custodial.
  • Execute/custodial register the transfer with the relayer: canton_to_ethereum, source_tx_hash = burn request UUID, burn_request_id metadata. Registration failure after a successful burn is reported as a tracking failure, not a withdrawal failure.
  • xreserve token config gains instrument_admin, instrument_id, withdraw_destination_domain (0 = Ethereum).

Relayer (bridges/xreserve)

  • stepWithdrawal: "" / awaiting_release → completed(released), polling Circle's burn status by request id. Same observer discipline as deposits: pending and transient-outage responses keep polling without consuming transfer retries; DestTxHash records the release tx.

Pinned-to-stub caveats (verify in #360)

  • BridgeUserAgreement_Burn choice/argument field names follow DA's devnet xReserve docs and the devstack stub.
  • Burn-status endpoint (/v1/burns/{requestId}) and burn-mint factory path mirror the registrar prefix the devstack stub implements; DA's hosted Utilities backend may differ.

Testing

  • SDK: burn arg encoding exercised through the shared converters (existing token tests still green; mocks regenerated for the 2 new interface methods)
  • bridgeapi: prepare/execute/custodial flows, single-use + ownership + fingerprint guards, validation matrix
  • xreserve: full withdrawal stage machine + burn-status client (released/pending/404/5xx)
  • golangci-lint clean across all touched packages; whole-module go test green (except two pre-existing Docker-dependent tests that panic without a local Docker daemon — they run in CI)

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.22798% with 223 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/bridge-api@3f9be3c). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/cantonsdk/token/burn.go 0.00% 112 Missing ⚠️
pkg/bridgeapi/http.go 0.00% 45 Missing ⚠️
pkg/bridgeapi/withdraw.go 75.00% 18 Missing and 18 partials ⚠️
pkg/cantonsdk/token/registry_client.go 0.00% 13 Missing ⚠️
pkg/relayer/bridges/xreserve/bridge.go 76.19% 9 Missing and 1 partial ⚠️
pkg/relayer/bridges/xreserve/circle.go 84.00% 3 Missing and 1 partial ⚠️
pkg/app/api/server.go 0.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##             feat/bridge-api     #376   +/-   ##
==================================================
  Coverage                   ?   35.52%           
==================================================
  Files                      ?      178           
  Lines                      ?    13930           
  Branches                   ?        0           
==================================================
  Hits                       ?     4948           
  Misses                     ?     8571           
  Partials                   ?      411           
Flag Coverage Δ
unittests 35.52% <42.22%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/bridgeapi/config.go 60.00% <ø> (ø)
pkg/bridgeapi/service.go 82.10% <100.00%> (ø)
pkg/cantonsdk/token/client.go 0.00% <ø> (ø)
pkg/cantonsdk/token/config.go 0.00% <ø> (ø)
pkg/cantonsdk/token/types.go 0.00% <ø> (ø)
pkg/relayer/types.go 100.00% <ø> (ø)
pkg/app/api/server.go 0.00% <0.00%> (ø)
pkg/relayer/bridges/xreserve/circle.go 88.23% <84.00%> (ø)
pkg/relayer/bridges/xreserve/bridge.go 75.15% <76.19%> (ø)
pkg/cantonsdk/token/registry_client.go 0.00% <0.00%> (ø)
... and 3 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Completes the USDCx bridge loop (#359): Canton burn -> Circle releases
USDC on Ethereum, relayer tracks the release.

Token SDK (pkg/cantonsdk/token):
- PrepareBurn / BurnByPartyID exercise BridgeUserAgreement_Burn on the
  user's agreement contract via Interactive Submission, reusing the
  existing prepare/execute and custodial signing machinery
- Holdings selection reuses the UTXO picker from the transfer path
- RegistryClient.GetBurnMintFactory fetches the factory + choice
  context + disclosed contracts (transfer-factory response envelope,
  shared parser)
- config: burn_mint_package_id / burn_mint_module gate the feature

Bridge API (pkg/bridgeapi):
- POST /api/v2/bridge/withdraw/prepare|execute (external keys) and
  /custodial - mirrors the transfer prepare/execute pattern incl.
  fingerprint check; single-use prepared-burn store
- execute/custodial register the transfer with the relayer
  (canton_to_ethereum, burn_request_id metadata)

Relayer (bridges/xreserve):
- stepWithdrawal: '' / awaiting_release -> completed(released), polling
  Circle burn status by request id; transient outages keep polling
  without burning transfer retries

Daml choice/argument names and the burn-status API shape are pinned to
the devstack stub; production verification tracked in #360.
- custodial and external withdrawals register the transfer with the
  relayer (keyed by a required idempotency_key) BEFORE the burn, so a
  client retry of a lost-response call returns the existing transfer
  instead of burning a second time (was: fund-losing double burn)
- WithdrawExecute validates owner/fingerprint/signature via a store Peek
  and only consumes the prepared burn once validation passes, so a bad
  request or a foreign caller can no longer destroy a user's pending burn
- burn holdings are filtered to the requested instrument admin before
  selection (was: could pick a same-id instrument from another issuer)
- findBridgeUserAgreement errors on >1 agreement instead of picking one
- xreserve withdrawals are reaped after a completion deadline, so a burn
  that never releases (or was never submitted) fails instead of polling
  forever
- 429/408 burn-status responses treated as transient
- burn_request_id metadata key shared via relayer.MetaBurnRequestID so
  writer (api-server) and reader (adapter) cannot drift
Add the withdraw xreserve fields (instrument_admin/instrument_id/
withdraw_destination_domain) to the docker bridge block, and a
documented burn_mint block in canton.token (uncomment with the confirmed
utility-bridge module name, #360).
@sadiq1971
sadiq1971 force-pushed the feat/usdcx-outbound branch from 90d6160 to 93d7d00 Compare July 29, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants