Skip to content

feat(relayer): xreserve adapter — USDCx inbound tracking - #374

Open
sadiq1971 wants to merge 5 commits into
feat/bridge-foundationfrom
feat/xreserve-inbound
Open

feat(relayer): xreserve adapter — USDCx inbound tracking#374
sadiq1971 wants to merge 5 commits into
feat/bridge-foundationfrom
feat/xreserve-inbound

Conversation

@sadiq1971

Copy link
Copy Markdown
Member

Part of #357 and epic #361. Stacked on #373 (TokenBridge foundation) — review the last commit only until #373 merges.

What

The first TokenBridge adapter: bridges/xreserve tracks USDCx deposits (Ethereum USDC → Canton USDCx via Circle xReserve), plus the relayer registration endpoint observer mechanisms need.

Design

xreserve is an observer mechanism — Circle executes the bridge, we are never in the critical path:

  • Sources() returns nothing; transfers are registered at initiation via the new internal POST /api/v1/transfers (called by the api-server when the dapp submits depositToRemote; feat(api): bridge API — deposit quotes, registration, unified status #358 adds that caller). Registration is idempotent and rejects bridge keys no adapter owns.
  • Step stage machine: "" → awaiting_attestation → awaiting_mint → completed (stage minted).
    • First step snapshots the recipient's instrument balance (matched on instrument admin + id via token.Token.GetHoldingsByParty, the Splice HoldingV1 interface query) — before Ethereum finality, so before any mint for this deposit can exist.
    • Attestation polling distinguishes not ready (finality window, keep polling) from unavailable (Circle outage — keep polling, log, don't burn transfer retries) from hard errors (step error → retry/backoff machinery).
    • Completion = balance grew by the deposited amount over the baseline; the mint itself is done by the recipient's pre-approved BridgeUserAgreement, never by us. Relayer downtime cannot affect bridging, only status display.

Config

bridge:
  tokens:
    USDCX:
      mechanism: xreserve
      evm_address: "0x..."
      decimals: 6
      xreserve:
        attestation_url: "http://usdcx-attestation:8091"
        instrument_admin: "${CANTON_USDCX_ISSUER_PARTY}"
        instrument_id: "USDCx"

Notes / caveats for review

  • Read rights: holding detection requires the relayer's Canton token to read user parties (CanReadAsAnyParty, same as the indexer). Devnet config change, called out in the deploy notes.
  • Attestation API schema is pinned to the shape the devstack stub will implement; Circle's production schema is an explicit chore(bridge): hardening + mainnet enablement #360 work item.
  • Baseline-based mint detection assumes registration happens promptly after the deposit tx is sent (well within the ~15 min attestation window) — documented in the package comment.

Remaining on #357 (follow-ups, not this PR)

Testing

  • Adapter: full stage-machine coverage incl. admin-mismatch filtering, transient-vs-hard attestation errors, unknown token/stage, withdrawal guard
  • Attestation client: httptest coverage for complete/pending/404/5xx/4xx/connection-refused
  • Service: registration create/idempotent-replay/validation matrix
  • golangci-lint clean; existing tests pass

@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.22449% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 35.52%. Comparing base (774a7d3) to head (947c804).

Files with missing lines Patch % Lines
pkg/relayer/bridges/xreserve/bridge.go 75.00% 23 Missing and 8 partials ⚠️
pkg/relayer/service/log.go 0.00% 25 Missing ⚠️
pkg/relayer/service/http.go 0.00% 24 Missing ⚠️
pkg/app/relayer/server.go 0.00% 18 Missing ⚠️
pkg/relayer/bridges/xreserve/circle.go 84.61% 3 Missing and 3 partials ⚠️
pkg/relayer/service/service.go 88.23% 4 Missing and 2 partials ⚠️
pkg/relayer/bridges/xreserve/holdings.go 69.23% 2 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                    Coverage Diff                     @@
##           feat/bridge-foundation     #374      +/-   ##
==========================================================
+ Coverage                   34.91%   35.52%   +0.60%     
==========================================================
  Files                         167      170       +3     
  Lines                       13004    13288     +284     
==========================================================
+ Hits                         4540     4720     +180     
- Misses                       8098     8187      +89     
- Partials                      366      381      +15     
Flag Coverage Δ
unittests 35.52% <61.22%> (+0.60%) ⬆️

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

Files with missing lines Coverage Δ
pkg/relayer/types.go 100.00% <ø> (ø)
pkg/relayer/bridges/xreserve/holdings.go 69.23% <69.23%> (ø)
pkg/relayer/bridges/xreserve/circle.go 84.61% <84.61%> (ø)
pkg/relayer/service/service.go 81.81% <88.23%> (+81.81%) ⬆️
pkg/app/relayer/server.go 0.00% <0.00%> (ø)
pkg/relayer/service/http.go 0.00% <0.00%> (ø)
pkg/relayer/service/log.go 0.00% <0.00%> (ø)
pkg/relayer/bridges/xreserve/bridge.go 75.00% <75.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

First adapter on the TokenBridge foundation (#357). Observer mechanism:
Circle executes the bridge, so Sources() is empty and Step only tracks
progress - transfers are registered at initiation via the relayer API.

- bridges/xreserve: deposit stage machine
  '' -> awaiting_attestation -> awaiting_mint -> completed(minted).
  Mint detection is balance-based against a baseline snapshotted on the
  first step (before Ethereum finality). Transient attestation-service
  failures keep polling instead of burning transfer retries.
- circle.go: attestation HTTP client (path/schema pinned to the devstack
  stub; Circle production schema tracked in #360)
- service: POST /api/v1/transfers registers observer-mechanism transfers
  (idempotent, validated against registered bridge keys)
- config: per-token xreserve block (attestation_url, instrument_admin,
  instrument_id, poll intervals)
- wiring: xreserve case in buildBridgeRegistry

Note: relayer's Canton token must carry read rights over user parties
(CanReadAsAnyParty) for holding detection, as the indexer already requires.
- reject non-positive amounts at registration (was: amount=0/-N completes
  a transfer instantly with no mint, fabricating a success record)
- idempotent replay only for the same owner: a conflicting bridge_key or
  sender returns 409 instead of disclosing the foreign row (IDOR)
- bearer-token guard on the internal registration endpoint (env
  RELAYER_REGISTRATION_TOKEN); compose binds the relayer API to loopback
  so it is not host-exposed
- deposits that never mint are failed after a completion deadline
  instead of polling forever and holding step-loop batch slots
- treat 429/408 (and 5xx) attestation responses as transient (keep
  polling) so a rate-limited but healthy deposit is not force-failed
- document the balance-delta detection limitation (concurrent same-
  recipient deposits / unrelated credits) as gated on #360 mint-event data
@sadiq1971
sadiq1971 force-pushed the feat/xreserve-inbound branch from dac0db3 to 864d5ff Compare July 27, 2026 12:29
@sadiq1971
sadiq1971 marked this pull request as ready for review July 29, 2026 09:50
The bridge.tokens schema landed but the shipped configs never used it.
Add a USDCx xreserve token to the docker + local-devnet relayer configs
(devstack stub host / Sepolia addresses); mainnet stays disabled pending
#360.
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