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

Labels

GrantFox OSSPart of the GrantFox OSS programMaybe RewardedPotential reward for completionOfficial Campaign | FWC26GrantFox official campaign FWC26complexity:highMaps to Drips Wave High tier (200 pts)enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions