Skip to content

Add pre-flight payment safety checks with SEP-29 memo detection - #68

Merged
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Tijesunimi004:feat/preflight-payment-safety-checks
Jul 23, 2026
Merged

Add pre-flight payment safety checks with SEP-29 memo detection#68
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Tijesunimi004:feat/preflight-payment-safety-checks

Conversation

@Tijesunimi004

@Tijesunimi004 Tijesunimi004 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #59.

  • Add preflightPayment() to stellarService.js: runs destination existence, USDC trustline, source USDC balance, source XLM reserve/fee, and SEP-29 config.memo_required checks in parallel and returns a structured { ok, reasons, warnings } verdict instead of throwing.
  • Add isMemoRequired() reading the destination account's data_attr["config.memo_required"] entry per SEP-29.
  • Gate initializePayment on preflightPayment: on failure it aborts the Mongo transaction, creates no Transaction row, and returns 400 with the structured reason codes. The happy path is unchanged.
  • Add POST /api/stellar/payment/preflight (getPaymentPreflight) so the frontend can show blocking issues before prompting the wallet to sign.
  • Centralize the platform's DNB-<TYPE>-<id8> memo construction into one buildPurchaseMemo() helper shared by both call sites, with the memo convention documented inline.
  • Submit-time result_codes handling in submitTransaction is left as-is as a backstop for races.

Test plan

  • test/preflightPayment.test.js — 14 tests covering each reason code individually, SEP-29 pass/fail, reserve math with extra operations (fee split), and multi-reason collection, all with server.loadAccount mocked (no live Horizon calls).
  • Ran the full existing suite; no regressions from this change (pre-existing local-only failures in pathPayments.test.js/payout.test.js/auth.test.js reproduce identically on the base commit and are unrelated environment issues, not caused by this diff).

Summary by CodeRabbit

  • New Features

    • Added payment preflight checks before wallet signing.
    • Validates destination availability, balances, trustlines, reserve requirements, fees, and required memos.
    • Added structured validation results with detailed failure reasons and warnings.
    • Standardized payment destination resolution and purchase memo formatting.
  • Bug Fixes

    • Prevents unsafe payment transactions from proceeding when validation fails.
  • Tests

    • Added coverage for payment validation, balance requirements, trustlines, reserves, fees, and memo rules.

Add preflightPayment() to stellarService.js, validating destination
existence, USDC trustline, source balance/reserve, and SEP-29
memo-required status before an unsigned XDR is built. Wire it into
initializePayment (400 with structured reasons, no Transaction row
created on failure) and expose it standalone via
POST /api/stellar/payment/preflight so the frontend can surface
blocking issues before prompting the wallet to sign.

Centralize the platform's DNB-<TYPE>-<id8> memo construction into a
single helper shared by both call sites.
Copilot AI review requested due to automatic review settings July 23, 2026 14:41
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

The Stellar payment flow adds structured preflight validation for destination existence, USDC trustlines, source balances, XLM reserve coverage, and SEP-29 memo requirements. Shared destination and memo helpers are used by both the preflight endpoint and payment initialization.

Stellar payment preflight

Layer / File(s) Summary
Account normalization and preflight checks
src/services/stellar/stellarService.js, test/preflightPayment.test.js
Normalizes Horizon account summaries and validates balances, reserves, trustlines, account existence, and SEP-29 memo requirements with structured reason codes and Jest coverage.
Payment destination resolution and safety gate
src/controllers/stellar/paymentController.js
Shares destination and purchase memo construction across flows, adds payment preflight handling, and blocks non-path payments when validation fails.
Authenticated preflight route wiring
src/routes/stellar/paymentRoutes.js
Registers the authenticated POST /preflight endpoint and connects it to getPaymentPreflight.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant paymentRoutes
  participant getPaymentPreflight
  participant preflightPayment
  participant Horizon
  Client->>paymentRoutes: POST /preflight
  paymentRoutes->>getPaymentPreflight: Invoke authenticated handler
  getPaymentPreflight->>preflightPayment: Validate payment details
  preflightPayment->>Horizon: Load source and destination accounts
  Horizon-->>preflightPayment: Return account data
  preflightPayment-->>getPaymentPreflight: Return validation verdict
  getPaymentPreflight-->>Client: Return preflight response
Loading

Possibly related PRs

Suggested reviewers: copilot, abimbolaalabi

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding pre-flight payment safety checks and SEP-29 memo detection.
Linked Issues check ✅ Passed The PR adds structured preflight validation, a preflight endpoint, initializePayment gating, and tests covering destination, trustline, balance, reserve, and memo checks.
Out of Scope Changes check ✅ Passed The refactors and helper additions appear directly tied to the new pre-flight validation flow and payment memo handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@zeemscript
zeemscript merged commit 768029d into Deen-Bridge:dev Jul 23, 2026
2 of 3 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a “pre-flight” safety layer to Stellar payments so the backend (and a new API endpoint) can detect common failure modes (missing destination, missing USDC trustline, insufficient balances/reserve, SEP-29 memo-required) before building an unsigned XDR and prompting a wallet signature.

Changes:

  • Added preflightPayment() + SEP-29 config.memo_required detection (isMemoRequired) to return a structured { ok, reasons, warnings } verdict.
  • Added a new protected endpoint POST /api/stellar/payment/preflight to let the frontend fetch blocking issues ahead of signing.
  • Gated initializePayment (non-path payments) on pre-flight checks and centralized purchase memo formatting via buildPurchaseMemo().

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/services/stellar/stellarService.js Adds preflight validation logic, SEP-29 memo-required detection, and shared parsing helpers for account balances/reserve calculations.
src/controllers/stellar/paymentController.js Adds a preflight controller endpoint, shared destination-resolution helper, centralized memo construction, and preflight gating in the initialize flow.
src/routes/stellar/paymentRoutes.js Exposes the new /preflight payment route.
test/preflightPayment.test.js Adds unit tests covering reason codes, SEP-29 behavior, reserve math, and multi-reason collection using mocked Horizon calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 372 to 373
// Build the payment transaction (single op full amount for platform collect, split for direct if fee configured)
const isPathPayment = sendAssetInput && sendMax;
Comment on lines +40 to +44
const Model = itemType === "book" ? Book : Course;
const populateField = itemType === "book" ? "author" : "createdBy";

const query = Model.findById(itemId).populate(populateField, "stellarWallet name");
const item = session ? await query.session(session) : await query;
Comment on lines +317 to +320
// Stellar protocol base reserve is 0.5 XLM per ledger entry (2 base entries
// per account, plus one more per subentry: trustlines, offers, signers, data).
const BASE_RESERVE_STROOPS = 5000000n;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/services/stellar/stellarService.js (1)

340-416: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid preflight implementation — verified the reserve/fee math by hand against the test cases.

Promise.all for parallel account loads, BigInt stroops throughout (no floats), and collecting all reasons instead of short-circuiting all check out. This satisfies the non-custodial/BigInt requirements for src/services/stellar/**.

One gap worth flagging: preflightPayment assumes the source is paying in USDC (SOURCE_INSUFFICIENT_BALANCE/SOURCE_INSUFFICIENT_RESERVE are computed against summary.usdcBalance). If this is ever called for a different source asset (e.g. from the path-payment flow), these checks would be meaningless or misleading. Currently paymentController.js correctly avoids calling this for path payments — but that means path payments get no preflight validation, including the asset-agnostic destination checks (existence/trustline/memo-required). See the corresponding comment on paymentController.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/stellar/stellarService.js` around lines 340 - 416, Update
preflightPayment to separate asset-agnostic destination validation from
source-asset-specific balance and reserve checks, so callers using non-USDC
assets do not receive misleading USDC insufficiency reasons. Preserve the
existing destination account, trustline, and memo checks, and adjust the calling
flow as needed to run those checks for path payments without applying
USDC-specific source validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/controllers/stellar/paymentController.js`:
- Around line 211-273: Extract the duplicated fee-split and preflightPayment
invocation from getPaymentPreflight and initializePayment into a shared
runPreflight helper. Have the helper accept the buyer wallet,
destinationPublicKey, item, memo, and settlementMode, preserve the existing
amount and operationCount behavior, and update both handlers to use it.
- Around line 211-273: The new getPaymentPreflight endpoint lacks
controller-level Jest coverage. Add endpoint tests that mock
resolvePaymentDestination and preflightPayment, covering invalid itemType,
missing Stellar wallet, free items, and a successful response asserting the 200
status and preflight payload shape; ensure the tests use the project’s
--experimental-vm-modules-compatible setup.
- Around line 401-420: Ensure the isPathPayment branch also performs
destination-side preflight validation before buildPathPaymentTransaction and
wallet signing. Refactor preflightPayment or extract a destination-only
validation helper so checks such as missing accounts, trustlines, and required
memos apply without assuming a USDC source asset; preserve the existing
source-asset checks for non-path payments and abort the session with the
existing failure response when validation fails.

---

Nitpick comments:
In `@src/services/stellar/stellarService.js`:
- Around line 340-416: Update preflightPayment to separate asset-agnostic
destination validation from source-asset-specific balance and reserve checks, so
callers using non-USDC assets do not receive misleading USDC insufficiency
reasons. Preserve the existing destination account, trustline, and memo checks,
and adjust the calling flow as needed to run those checks for path payments
without applying USDC-specific source validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b64b3bd2-60e5-48f9-b1a4-a307ffeacf18

📥 Commits

Reviewing files that changed from the base of the PR and between 2866f7b and 491c734.

📒 Files selected for processing (4)
  • src/controllers/stellar/paymentController.js
  • src/routes/stellar/paymentRoutes.js
  • src/services/stellar/stellarService.js
  • test/preflightPayment.test.js

Comment on lines +211 to +273
export const getPaymentPreflight = async (req, res) => {
try {
const buyerId = req.user._id;
const { itemType, itemId } = req.body;

if (!["book", "course"].includes(itemType)) {
return res.status(400).json({
success: false,
message: "Invalid item type. Must be 'book' or 'course'",
});
}

const buyer = await User.findById(buyerId);
if (!buyer?.stellarWallet?.publicKey) {
return res.status(400).json({
success: false,
message: "Please connect your Stellar wallet first",
});
}

const resolved = await resolvePaymentDestination({ itemType, itemId });
if (resolved.error) {
return res.status(resolved.error.status).json({
success: false,
message: resolved.error.message,
});
}

const { item, destinationPublicKey, settlementMode } = resolved;

if (!item.price || item.price === 0) {
return res.status(400).json({
success: false,
message: "This item is free, no payment required",
});
}

const memo = buildPurchaseMemo(itemType, itemId);
const feeSplitPreview =
settlementMode === "direct" ? calculateFeeSplit(item.price) : null;

const preflight = await preflightPayment({
sourcePublicKey: buyer.stellarWallet.publicKey,
destinationPublicKey,
amount: item.price.toString(),
memo,
operationCount: feeSplitPreview ? 2 : 1,
});

res.status(200).json({
success: true,
preflight,
});
} catch (error) {
logger.error("Payment preflight error:", error);
res.status(500).json({
success: false,
message: "Failed to run payment pre-flight checks",
error:
process.env.NODE_ENV === "development" ? error.message : undefined,
});
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Endpoint logic looks correct, but this handler and initializePayment's new preflight block (Lines 401-410) are byte-for-byte duplicated logic.

Both compute feeSplitPreview and call preflightPayment with the identical shape. If the operationCount logic or preflight params ever need a fix (e.g. adding sendAsset support), it's easy to update one call site and forget the other, silently reintroducing the exact race this PR is trying to close.

Consider extracting a shared helper, e.g.:

♻️ Suggested extraction
const runPreflight = async ({ buyerWallet, destinationPublicKey, item, memo, settlementMode }) => {
  const feeSplitPreview =
    settlementMode === "direct" ? calculateFeeSplit(item.price) : null;

  return preflightPayment({
    sourcePublicKey: buyerWallet,
    destinationPublicKey,
    amount: item.price.toString(),
    memo,
    operationCount: feeSplitPreview ? 2 : 1,
  });
};

Then call runPreflight(...) from both getPaymentPreflight and initializePayment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/stellar/paymentController.js` around lines 211 - 273, Extract
the duplicated fee-split and preflightPayment invocation from
getPaymentPreflight and initializePayment into a shared runPreflight helper.
Have the helper accept the buyer wallet, destinationPublicKey, item, memo, and
settlementMode, preserve the existing amount and operationCount behavior, and
update both handlers to use it.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

No Jest coverage found for the new getPaymentPreflight endpoint.

The only tests supplied (test/preflightPayment.test.js) exercise stellarService.preflightPayment/isMemoRequired directly, not the controller/route. Given this endpoint drives frontend payment gating, an endpoint-level test (mocking resolvePaymentDestination/preflightPayment, asserting the 400s for invalid itemType, missing wallet, free item, and the 200 success shape) would close the gap.

As per path instructions, "New or changed endpoints need Jest coverage (the runner uses --experimental-vm-modules)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/stellar/paymentController.js` around lines 211 - 273, The new
getPaymentPreflight endpoint lacks controller-level Jest coverage. Add endpoint
tests that mock resolvePaymentDestination and preflightPayment, covering invalid
itemType, missing Stellar wallet, free items, and a successful response
asserting the 200 status and preflight payload shape; ensure the tests use the
project’s --experimental-vm-modules-compatible setup.

Source: Path instructions

Comment on lines +401 to +420
const feeSplitPreview =
settlementMode === "direct" ? calculateFeeSplit(item.price) : null;

const preflight = await preflightPayment({
sourcePublicKey: buyer.stellarWallet.publicKey,
destinationPublicKey,
amount: item.price.toString(),
memo,
operationCount: feeSplitPreview ? 2 : 1,
});

if (!preflight.ok) {
await session.abortTransaction();
return res.status(400).json({
success: false,
message: "Payment failed pre-flight safety checks",
reasons: preflight.reasons,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preflight gate correctly blocks and aborts on failure — but it only runs for the non-path-payment branch.

isPathPayment (Line 373) transactions skip preflightPayment entirely and go straight to buildPathPaymentTransaction. That means for path payments, none of the destination checks (DESTINATION_ACCOUNT_MISSING, DESTINATION_NO_TRUSTLINE, DESTINATION_MEMO_REQUIRED) — which are asset-agnostic and would apply regardless of the source asset — are validated before the wallet is asked to sign. Issue #59 explicitly calls for pre-flight validation "before unsigned XDR creation and wallet signing," without carving out an exception for path payments.

I understand preflightPayment's source-side checks (USDC balance/reserve) can't apply as-is to path payments since the source asset may not be USDC — but the destination-side checks could still run. Worth either:

  1. Splitting preflightPayment so destination-only checks can be invoked independently of source-asset assumptions, or
  2. Explicitly documenting that path payments intentionally rely solely on the submit-time result-code backstop for now.

Right now the fallback (submit-time result-code handling) does still catch this at signing/submission time, which limits the blast radius — but the wallet will still be asked to sign a doomed-to-fail transaction, which is exactly what this PR set out to prevent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/stellar/paymentController.js` around lines 401 - 420, Ensure
the isPathPayment branch also performs destination-side preflight validation
before buildPathPaymentTransaction and wallet signing. Refactor preflightPayment
or extract a destination-only validation helper so checks such as missing
accounts, trustlines, and required memos apply without assuming a USDC source
asset; preserve the existing source-asset checks for non-path payments and abort
the session with the existing failure response when validation fails.

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.

3 participants