Skip to content

[Enhancement] SEP-24 anchor integration: non-custodial fiat on/off-ramp for USDC via interactive deposit and withdrawal #46

Description

@zeemscript

Summary

Give users a fiat on/off-ramp for USDC by integrating SEP-24 hosted deposit/withdrawal as a client of Stellar anchors: discover an anchor's capabilities from its stellar.toml, authenticate the user's own wallet to the anchor via SEP-10 (challenge signed client-side — the backend never holds keys), launch the anchor's interactive deposit/withdraw flow, and track the anchor transaction to completion with a local record the frontend can poll. Ships wired to SDF's testnet reference anchor so the whole flow is demoable end-to-end without touching real money.

Current state

  • There is no on/off-ramp anywhere: a user can only fund their wallet outside the product. src/services/stellar/stellarService.js covers balances, USDC payment build/submit/verify, and explorer URLs — nothing anchor-related, no stellar.toml handling, no SEP-24/SEP-6/SEP-12 code in the repo.
  • The wallet model is strictly non-custodial: users connect a public key (User.stellarWallet.publicKey, src/models/User.js; connectWallet in src/controllers/stellar/walletController.js) and sign XDR in the browser (initializePayment → client signs → submitPayment in src/controllers/stellar/paymentController.js). Any anchor integration must keep signing on the client.
  • Issue [Enhancement] SEP-10 Web Authentication: challenge/verify endpoints and JWT issuance for "Sign in with Stellar" #25 / open PR work covers a SEP-10 server (DeenBridge issuing its own challenges). This issue is the inverse role — DeenBridge as a SEP-10 client obtaining a JWT from an anchor for the user's account — and shares no endpoints with it.
  • @stellar/stellar-sdk v16 (package.json) already ships the needed primitives: StellarToml.Resolver.resolve(homeDomain) for TOML discovery and XDR tooling to relay/inspect challenge transactions; axios is available for the anchor's REST endpoints.
  • The trustline problem SEP-24 deposits solve is already visible in the code: submitTransaction maps op_no_trust to a hard failure, and getAccountBalance reports hasTrustline — a first-time depositor typically needs a changeTrust before the anchor can send USDC.

What to build

  1. Config (src/config/validateEnv.js optionalEnvVars, .env.example): ANCHOR_HOME_DOMAINS (comma-separated allowlist, default testanchor.stellar.org on testnet), ANCHOR_TOML_CACHE_TTL (default 1h). Endpoints return 503 if the feature is unconfigured — the server must still boot without it (CI boot check).
  2. Anchor discovery service (src/services/stellar/anchorService.js): getAnchorInfo(homeDomain) — resolve and cache stellar.toml (via StellarToml.Resolver), extract TRANSFER_SERVER_SEP0024, WEB_AUTH_ENDPOINT, SIGNING_KEY, and the anchor's USDC currency entry; verify the anchor's USDC issuer matches the platform's USDC_ISSUER (refuse mismatches). Fetch and normalize GET {TRANSFER_SERVER_SEP0024}/info (enabled assets, fee/limits fields). Only domains on the allowlist are ever contacted.
  3. SEP-10 client auth relay (the non-custodial hinge, in anchorService.js + routes):
    • POST /api/stellar/anchor/auth/challenge — body { homeDomain }; backend fetches GET {WEB_AUTH_ENDPOINT}?account=<user's stellarWallet.publicKey>, validates the returned challenge XDR before relaying it (sequence number 0, signed by the TOML SIGNING_KEY, correct network passphrase and home domain — never blindly forward a transaction for the user to sign), and returns the XDR to the frontend.
    • POST /api/stellar/anchor/auth/verify — body { homeDomain, signedXdr }; backend POSTs it to the anchor, receives the anchor JWT, and stores it server-side (encrypted at rest or in Redis with the token's own expiry — never returned raw to the client) keyed by (userId, homeDomain).
  4. Interactive flows (src/routes/stellar/anchorRoutes.js mounted at /api/stellar/anchor in app.js, behind protect):
    • POST /api/stellar/anchor/deposits and POST /api/stellar/anchor/withdrawals — call the anchor's POST /transactions/deposit/interactive (or /withdraw/interactive) with the stored JWT, asset_code=USDC, and the user's account; persist an AnchorTransaction and return the anchor's interactive url + id for the frontend to open in a popup/iframe per SEP-24.
    • For deposits to an account without a USDC trustline, include the existing build-XDR pattern to offer a changeTrust transaction first (reuse networkPassphrase/USDC from stellarService.js).
  5. Tracking (src/models/AnchorTransaction.js): user, homeDomain, kind (deposit/withdrawal), anchor transactionId, status (mirror SEP-24 statuses: incomplete, pending_user_transfer_start, pending_anchor, pending_stellar, completed, refunded, error, ...), amounts/fees as strings, stellarTxHash when the anchor reports one, timestamps. A poller (interval loop, restart-safe, aligned with the worker patterns of issues [Enhancement] Horizon payment ingestion and reconciliation worker for unreported on-chain payments #26/[Enhancement] Background job queue for transactional email, receipts, and Horizon verification retries #32 but not dependent on them) refreshes non-terminal records via GET {TRANSFER_SERVER_SEP0024}/transaction?id=... with backoff, and stops on terminal states.
    • GET /api/stellar/anchor/transactions (paginated, own records only) and GET /api/stellar/anchor/transactions/:id (live-refreshes on read if stale).
  6. Docs: a short docs/anchors.md covering the trust model (anchor JWTs are held server-side; user keys never leave the wallet), how to add a mainnet anchor to the allowlist, and the testnet demo script against testanchor.stellar.org.

Acceptance criteria

  • GET-ing anchor info for an allowlisted domain returns parsed TOML + /info data, cached with TTL; non-allowlisted domains are rejected with 403 before any network call; a USDC issuer mismatch between anchor and platform is refused with a clear error.
  • Challenge relay validates the anchor's challenge (wrong signing key, wrong network, nonzero sequence, or wrong home domain → 502 with reason, nothing returned to sign); the signed challenge round-trip yields an anchor JWT stored server-side and never exposed in any API response.
  • Starting a deposit returns a working interactive URL against testanchor.stellar.org on testnet, and an AnchorTransaction row in incomplete; the poller advances it through anchor statuses to a terminal state without manual intervention.
  • Users can only list/read their own anchor transactions; unconfigured feature returns 503 (server still boots — CI boot check stays green).
  • A deposit initiated for a wallet without a USDC trustline includes an unsigned changeTrust XDR in the response using the platform's USDC asset.
  • Withdrawal flow returns the interactive URL and tracks pending_user_transfer_start → completion; the Stellar payment leg remains user-signed (no server keys anywhere — verified by code review checklist in the PR).
  • Jest tests with mocked TOML/anchor HTTP (no live network in CI): challenge validation rejection matrix, issuer mismatch refusal, status polling state machine, and JWT storage redaction.

Pointers

Difficulty

High — a multi-protocol integration (SEP-1 + SEP-10 client + SEP-24) with a security-sensitive challenge-relay step, server-side credential custody for anchor JWTs, and a long-running status state machine, all constrained by the non-custodial signing model.


🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the dev branch. Quality bar: CI must stay green.

💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions