Skip to content

[Enhancement] Pre-flight payment safety: trustline, destination, and SEP-29 memo-required checks before signing #59

Description

@zeemscript

Summary

Add a pre-flight payment safety layer that validates a Stellar payment before the unsigned XDR is built and handed to the user's wallet: verify the recipient account exists, holds a USDC trustline, and — critically — enforce the SEP-29 "memo required" check so the platform never asks a user to sign a payment that will bounce or, worse, land at an exchange/custodial destination without the required memo. Today all of these failure modes are only discovered after the user has already signed and submitted, surfacing as raw op_no_trust / op_no_destination errors from Horizon.

Current state

  • buildPaymentTransaction in src/services/stellar/stellarService.js calls server.loadAccount(sourcePublicKey) and immediately builds a payment op to destinationPublicKey with Memo.text(memo || "DeenBridge Purchase"). It does no check that the destination exists, has a USDC trustline, or requires a memo of its own.
  • Trustline/destination problems are caught only at submit time: submitTransaction inspects error.response.data.extras.result_codes and rethrows friendly strings for op_underfunded, op_no_trust, and op_no_destination. By then the user has signed and broadcast — wasted round-trip and a confusing UX.
  • There is a helper hasUsdcTrustline(publicKey) and getAccountBalance (which returns hasTrustline), but initializePayment (src/controllers/stellar/paymentController.js) never calls them — it goes straight from "creator has a stellarWallet.publicKey" to buildPaymentTransaction.
  • SEP-29 is entirely unimplemented. Nothing reads the destination account's data entry / config.memo_required flag. A payment to a memo-required account (common for custodial wallets and exchanges an educator might use as a payout address) will be rejected on-chain with tx_bad_seq/op failures or silently misattributed.
  • The memo the platform sets is a fixed text tag (DNB-<TYPE>-<id8>); there is no validation that a caller-supplied destination memo (when the creator's wallet demands one) is present or well-formed.

What to build

  1. Pre-flight service (preflightPayment(...) in src/services/stellar/stellarService.js): given { sourcePublicKey, destinationPublicKey, amount, asset }, run in parallel:
    • destination exists (loadAccount, treat 404 as no_destination);
    • destination holds the USDC trustline (reuse getAccountBalance().hasTrustline);
    • source has sufficient USDC balance and enough XLM reserve to keep the tx valid;
    • SEP-29 memo-required: check the destination account's data_attr / manageData config.memo_required marker per the SEP-29 spec; if required, demand a memo and reject text/empty memos where an id/hash memo is expected.
      Return a structured verdict { ok, reasons: [...], warnings: [...] } rather than throwing.
  2. New endpoint GET /api/stellar/payment/preflight (or POST with { itemType, itemId }) in paymentController.js + paymentRoutes.js: resolves the creator/destination from the item (same lookup initializePayment already does), runs preflightPayment, and returns a human-readable check list the frontend can render before prompting the wallet — e.g. "Recipient needs to add a USDC trustline."
  3. Gate initializePayment: call preflightPayment before buildPaymentTransaction; if !ok, return 400 with the structured reasons and do not create a pending Transaction row. Keep the existing duplicate-purchase / already-owned / free-item checks.
  4. Memo standard: centralize memo construction and enforce SEP-29 — when the destination requires a memo, require the caller to pass one and set the correct Memo type; otherwise keep the current DNB-... text memo. Document the platform's memo convention.
  5. Keep submit-time handling as a backstop (races happen), but the happy path should now almost never hit op_no_trust/op_no_destination.

Acceptance criteria

  • preflightPayment returns a structured verdict covering: destination missing, no USDC trustline on destination, insufficient source USDC, insufficient XLM reserve, and SEP-29 memo-required — each as a distinct machine-readable reason code.
  • SEP-29 memo-required detection works: a payment to a memo-required account without an appropriate memo is rejected pre-flight (verified against a testnet account configured with config.memo_required), and a compliant memo passes.
  • POST /api/stellar/payment/initialize returns 400 with reasons and creates no Transaction when pre-flight fails; the happy path is unchanged when all checks pass.
  • A pre-flight endpoint lets the frontend show blocking issues before the wallet signature prompt.
  • Jest tests stub the Stellar SDK / Horizon (no live network in unit tests) to assert each reason code; any integration test that hits testnet is opt-in via env so CI stays green offline.

Pointers

Difficulty

High — requires correct SEP-29 semantics, careful Horizon error handling and reserve math, parallelized account lookups, and a non-breaking refactor of the initialize flow, all while keeping the network-touching code testable without a live Horizon.


🏆 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