You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
@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
Config (src/config/validateEnv.jsoptionalEnvVars, .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).
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.
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).
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).
GET /api/stellar/anchor/transactions (paginated, own records only) and GET /api/stellar/anchor/transactions/:id (live-refreshes on read if stale).
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.
Gotchas: in stellar-sdk v16 TOML resolution lives under the StellarToml namespace. Anchor JWTs are bearer credentials for the user's anchor session — treat them like passwords (encrypt at rest, expire with the token's exp). SEP-24 status vocabulary must be stored verbatim (don't collapse statuses; the frontend needs them). CI (.github/workflows/ci.yml) has no outbound network, so anchor HTTP must be mockable via injected base URLs. PRs target dev.
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
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
src/services/stellar/stellarService.jscovers balances, USDC payment build/submit/verify, and explorer URLs — nothing anchor-related, nostellar.tomlhandling, no SEP-24/SEP-6/SEP-12 code in the repo.User.stellarWallet.publicKey,src/models/User.js;connectWalletinsrc/controllers/stellar/walletController.js) and sign XDR in the browser (initializePayment→ client signs →submitPaymentinsrc/controllers/stellar/paymentController.js). Any anchor integration must keep signing on the client.@stellar/stellar-sdkv16 (package.json) already ships the needed primitives:StellarToml.Resolver.resolve(homeDomain)for TOML discovery and XDR tooling to relay/inspect challenge transactions;axiosis available for the anchor's REST endpoints.submitTransactionmapsop_no_trustto a hard failure, andgetAccountBalancereportshasTrustline— a first-time depositor typically needs achangeTrustbefore the anchor can send USDC.What to build
src/config/validateEnv.jsoptionalEnvVars,.env.example):ANCHOR_HOME_DOMAINS(comma-separated allowlist, defaulttestanchor.stellar.orgon 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).src/services/stellar/anchorService.js):getAnchorInfo(homeDomain)— resolve and cachestellar.toml(viaStellarToml.Resolver), extractTRANSFER_SERVER_SEP0024,WEB_AUTH_ENDPOINT,SIGNING_KEY, and the anchor's USDC currency entry; verify the anchor's USDC issuer matches the platform'sUSDC_ISSUER(refuse mismatches). Fetch and normalizeGET {TRANSFER_SERVER_SEP0024}/info(enabled assets, fee/limits fields). Only domains on the allowlist are ever contacted.anchorService.js+ routes):POST /api/stellar/anchor/auth/challenge— body{ homeDomain }; backend fetchesGET {WEB_AUTH_ENDPOINT}?account=<user's stellarWallet.publicKey>, validates the returned challenge XDR before relaying it (sequence number 0, signed by the TOMLSIGNING_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).src/routes/stellar/anchorRoutes.jsmounted at/api/stellar/anchorinapp.js, behindprotect):POST /api/stellar/anchor/depositsandPOST /api/stellar/anchor/withdrawals— call the anchor'sPOST /transactions/deposit/interactive(or/withdraw/interactive) with the stored JWT,asset_code=USDC, and the user'saccount; persist anAnchorTransactionand return the anchor's interactiveurl+idfor the frontend to open in a popup/iframe per SEP-24.changeTrusttransaction first (reusenetworkPassphrase/USDCfromstellarService.js).src/models/AnchorTransaction.js):user,homeDomain,kind(deposit/withdrawal), anchortransactionId,status(mirror SEP-24 statuses:incomplete,pending_user_transfer_start,pending_anchor,pending_stellar,completed,refunded,error, ...), amounts/fees as strings,stellarTxHashwhen 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 viaGET {TRANSFER_SERVER_SEP0024}/transaction?id=...with backoff, and stops on terminal states.GET /api/stellar/anchor/transactions(paginated, own records only) andGET /api/stellar/anchor/transactions/:id(live-refreshes on read if stale).docs/anchors.mdcovering 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 againsttestanchor.stellar.org.Acceptance criteria
GET-ing anchor info for an allowlisted domain returns parsed TOML +/infodata, 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.testanchor.stellar.orgon testnet, and anAnchorTransactionrow inincomplete; the poller advances it through anchor statuses to a terminal state without manual intervention.changeTrustXDR in the response using the platform's USDC asset.pending_user_transfer_start→ completion; the Stellar payment leg remains user-signed (no server keys anywhere — verified by code review checklist in the PR).Pointers
src/services/stellar/stellarService.js(USDC,USDC_ISSUER,networkPassphrase,NETWORK, trustline detection ingetAccountBalance),src/controllers/stellar/walletController.js,src/controllers/stellar/paymentController.js(build → client-sign → submit pattern to mirror),src/models/User.js(stellarWallet),src/config/validateEnv.js,app.js,src/utils/cache.js(TOML/JWT caching).StellarTomlnamespace. Anchor JWTs are bearer credentials for the user's anchor session — treat them like passwords (encrypt at rest, expire with the token'sexp). SEP-24 status vocabulary must be stored verbatim (don't collapse statuses; the frontend needs them). CI (.github/workflows/ci.yml) has no outbound network, so anchor HTTP must be mockable via injected base URLs. PRs targetdev.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
devbranch. Quality bar: CI must stay green.💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0