Skip to content

stellar: validate signed XDR contents before submit; store expectedHa… - #51

Open
Banx17 wants to merge 1 commit into
Deen-Bridge:devfrom
Banx17:feature/issue-18-verify-stellar-xdr
Open

stellar: validate signed XDR contents before submit; store expectedHa…#51
Banx17 wants to merge 1 commit into
Deen-Bridge:devfrom
Banx17:feature/issue-18-verify-stellar-xdr

Conversation

@Banx17

@Banx17 Banx17 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Closes #18

Summary by CodeRabbit

  • New Features

    • Added validation for signed Stellar payments, including memo, sender, destination, and amount details.
    • Payment and donation submissions are now verified before being sent to the Stellar network.
  • Bug Fixes

    • Invalid or tampered payment details are rejected with a clear validation error.
    • Failed submissions are recorded appropriately instead of being marked as successful.
    • Improved handling of payment transaction identifiers and memos.

…sh/memo; verify on-chain before granting access; add tests
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Jest now uses an in-memory MongoDB setup. Stellar payment and donation records persist expected hashes and memos, while signed XDR is validated against expected payment details before submission.

Changes

Stellar payment integrity

Layer / File(s) Summary
Jest test environment
jest.config.js, test/jest.setup.js, src/config/validateEnv.js
Jest starts an in-memory MongoDB server and supplies test environment defaults with teardown cleanup.
Expected transaction metadata
src/models/Transaction.js, src/controllers/stellar/paymentController.js, src/controllers/stellar/donationController.js
Pending transactions store expected hashes and memos while allowing the network transaction hash to remain unset.
Signed XDR validation and submission
src/services/stellar/stellarService.js, src/controllers/stellar/paymentController.js, src/controllers/stellar/donationController.js
Signed XDR is checked for memo, source, destinations, and normalized payment amounts; invalid submissions are failed and rejected with HTTP 400.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant paymentController
  participant validateSignedPaymentXdr
  participant Stellar
  Client->>paymentController: Submit signed XDR
  paymentController->>validateSignedPaymentXdr: Check expected payments, memo, and source
  validateSignedPaymentXdr-->>paymentController: Return parsed transaction or error
  paymentController->>Stellar: Submit validated XDR
  Stellar-->>paymentController: Return submission result
Loading

Possibly related PRs

Suggested reviewers: zeemscript, abimbolaalabi

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Pre-submit validation and hash/memo storage are present, but the required tampered-XDR tests and explicit access-gating confirmation aren't shown. Add automated tampered-XDR tests and verify the confirmed-on-chain step gates access before status changes to confirmed.
Out of Scope Changes check ⚠️ Warning The donationController validation changes extend the fix to a separate donation flow, which isn't part of issue #18's payment/access scope. Split the donation-related Stellar validation into a separate PR or link a matching issue before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title reflects the main change: pre-submit Stellar XDR validation and expected-hash storage.
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.

@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)

409-420: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the public Memo API over _type/_value.

tx.memo._type/tx.memo._value reach into the SDK's private storage fields instead of the documented memo.type/memo.value getters. It works today (the getters just return these fields), but it's not the guaranteed public contract and could silently break if the SDK internals change.

♻️ Proposed refactor to use public getters
   if (expectedMemo) {
     const memo = tx.memo;
     let memoText = null;
-    if (memo && memo._type === "text") {
-      const val = memo._value;
+    if (memo && memo.type === "text") {
+      const val = memo.value;
       memoText = Buffer.isBuffer(val) ? val.toString() : String(val);
     }
🤖 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 409 - 420, Update the
memo validation logic in the expectedMemo block to use the public tx.memo.type
and tx.memo.value getters instead of the private _type and _value fields.
Preserve the existing text-type check, Buffer/string conversion, and mismatch
error behavior.
🤖 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/config/validateEnv.js`:
- Around line 34-38: Centralize test environment setup before application
imports: in src/config/validateEnv.js, remove the test-mode assignments for
JWT_SECRET, MONGO_URI, and PORT so validateEnv() only consumes preconfigured
values; in test/jest.setup.js, preserve the generated MongoMemoryServer URI
while moving the static JWT secret and port defaults into the earliest
environment bootstrap path.

In `@src/controllers/stellar/paymentController.js`:
- Around line 433-477: Update validateSignedPaymentXdr in stellarService.js to
recognize and validate path_payment_strict_receive operations in addition to
regular payment operations. Match path payments against the expected destination
and amount using the operation’s path-payment fields, while preserving existing
validation behavior for standard payments so legitimate path-payment XDRs pass
pre-submission validation.

In `@src/services/stellar/stellarService.js`:
- Around line 389-453: Update validateSignedPaymentXdr to include
pathPaymentStrictReceive operations alongside payment operations, mirroring the
operation handling in verifyPaymentOperations. For path payments, compare the
destination asset, destination amount, and destination address against each
expected payment while preserving the existing native/USDC payment matching
behavior.

---

Nitpick comments:
In `@src/services/stellar/stellarService.js`:
- Around line 409-420: Update the memo validation logic in the expectedMemo
block to use the public tx.memo.type and tx.memo.value getters instead of the
private _type and _value fields. Preserve the existing text-type check,
Buffer/string conversion, and mismatch error behavior.
🪄 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: 708eb564-bb9f-4865-afe3-c1cdc522109a

📥 Commits

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

📒 Files selected for processing (7)
  • jest.config.js
  • src/config/validateEnv.js
  • src/controllers/stellar/donationController.js
  • src/controllers/stellar/paymentController.js
  • src/models/Transaction.js
  • src/services/stellar/stellarService.js
  • test/jest.setup.js

Comment thread src/config/validateEnv.js
Comment on lines +34 to +38
// Test mode: provide defaults for development of tests
if (process.env.NODE_ENV === "test") {
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-secret-key-at-least-32-characters-long";
process.env.MONGO_URI = process.env.MONGO_URI || "mongodb://test-db:27017/dnb-test";
process.env.PORT = process.env.PORT || "5000";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Centralize test configuration before application imports.

The two files duplicate source-controlled test defaults, and validateEnv() can run before beforeAll publishes the in-memory URI. Remove hardcoded credentials/configuration and establish the test environment before importing application modules.

  • src/config/validateEnv.js#L34-L38: remove the JWT, Mongo URI, and port literals; require values from test bootstrap/environment.
  • test/jest.setup.js#L11-L14: retain the generated MongoMemoryServer URI, but move static defaults to environment bootstrap that runs before app imports.
📍 Affects 2 files
  • src/config/validateEnv.js#L34-L38 (this comment)
  • test/jest.setup.js#L11-L14
🤖 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/config/validateEnv.js` around lines 34 - 38, Centralize test environment
setup before application imports: in src/config/validateEnv.js, remove the
test-mode assignments for JWT_SECRET, MONGO_URI, and PORT so validateEnv() only
consumes preconfigured values; in test/jest.setup.js, preserve the generated
MongoMemoryServer URI while moving the static JWT secret and port defaults into
the earliest environment bootstrap path.

Source: Path instructions

Comment on lines +433 to +477
// Build expected payments to validate XDR BEFORE submit
let expectedPayments = transaction.platformFee?.platformAmount
? [
{
destination: transaction.creatorWallet,
amount: transaction.platformFee.creatorAmount,
},
{
destination: transaction.platformFee.platformWallet,
amount: transaction.platformFee.platformAmount,
},
]
: [
{
destination: transaction.creatorWallet,
amount: transaction.amount,
},
];

// Validate signed XDR contents (memo, payments, optional source)
try {
validateSignedPaymentXdr(
signedXdr,
expectedPayments,
transaction.memo,
transaction.buyerWallet,
true
);
} catch (validationError) {
transaction.status = "failed";
transaction.failureReason = `validation_failed: ${validationError.message}`;
await transaction.save({ session });
await session.commitTransaction();
paymentsFailed.inc({ type: "purchase", reason: "validation_failed" });

logger.error(`Transaction ${transactionId} validation failed:`, validationError.message);

return res.status(400).json({
success: false,
message: "Signed transaction does not match expected payment details",
error: validationError.message,
});
}

// Update status to submitted after validation

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

Path payments will always fail this pre-submission check.

expectedPayments here doesn't account for path payments (see isPathPayment branch at Lines 278-304/295-304, which builds a path_payment_strict_receive operation via buildPathPaymentTransaction). validateSignedPaymentXdr in stellarService.js only matches on op.type === "payment", so a signed path-payment XDR will never find a match and every legitimate path payment will be rejected with 400 and marked failed. Root cause and fix belong in stellarService.js's validateSignedPaymentXdr — see that file's review for details.

🤖 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 433 - 477, Update
validateSignedPaymentXdr in stellarService.js to recognize and validate
path_payment_strict_receive operations in addition to regular payment
operations. Match path payments against the expected destination and amount
using the operation’s path-payment fields, while preserving existing validation
behavior for standard payments so legitimate path-payment XDRs pass
pre-submission validation.

Comment on lines +389 to +453
/**
* Validate a signed transaction XDR against expected payments and memo/source
* @param {string} signedXdr
* @param {Array<{destination:string, amount:string}>} expectedPayments
* @param {string} expectedMemo
* @param {string} expectedSource
* @param {boolean} requireSource
*/
export const validateSignedPaymentXdr = (
signedXdr,
expectedPayments = [],
expectedMemo,
expectedSource,
requireSource = true
) => {
const tx = StellarSdk.TransactionBuilder.fromXDR(
signedXdr,
networkPassphrase
);

// Memo check
if (expectedMemo) {
const memo = tx.memo;
let memoText = null;
if (memo && memo._type === "text") {
const val = memo._value;
memoText = Buffer.isBuffer(val) ? val.toString() : String(val);
}
if (memoText !== expectedMemo) {
throw new Error("Memo mismatch");
}
}

// Source check
if (requireSource && expectedSource) {
if (tx.source !== expectedSource) {
throw new Error("Source account mismatch");
}
}

// Payment operations check
const paymentOps = tx.operations.filter((op) => op.type === "payment");

for (const expected of expectedPayments) {
const match = paymentOps.find((op) => {
const assetMatches =
(op.asset && op.asset.code === "USDC" && op.asset.issuer === USDC_ISSUER) ||
(op.asset_type === "credit_alphanum4" && op.asset?.code === "USDC" && op.asset?.issuer === USDC_ISSUER);

const amountMatches = toStroops(op.amount) === toStroops(expected.amount);
const destMatches = op.destination === expected.destination;

return assetMatches && amountMatches && destMatches;
});

if (!match) {
throw new Error(
`Signed XDR missing expected USDC payment of ${expected.amount} to ${expected.destination}`
);
}
}

return tx;
};

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
ast-grep outline src/services/stellar/stellarService.js --view expanded || true

# Show the target region with line numbers.
sed -n '340,560p' src/services/stellar/stellarService.js | cat -n

# Find the path-payment builder / initializer references.
rg -n "buildPathPaymentTransaction|isPathPayment|validateSignedPaymentXdr|verifyPaymentOperations|path_payment_strict_receive|pathPaymentStrictReceive|toStroops" src/services/stellar/stellarService.js

Repository: Deen-Bridge/dnb-backend

Length of output: 10474


validateSignedPaymentXdr should accept path payments.
paymentOps only keeps op.type === "payment", so valid pathPaymentStrictReceive XDRs fail this pre-submission check and get marked failed before the on-chain verifier runs. Mirror the verifyPaymentOperations branch here and compare the path-payment destination asset and amount.

🤖 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 389 - 453, Update
validateSignedPaymentXdr to include pathPaymentStrictReceive operations
alongside payment operations, mirroring the operation handling in
verifyPaymentOperations. For path payments, compare the destination asset,
destination amount, and destination address against each expected payment while
preserving the existing native/USDC payment matching behavior.

Source: Path instructions

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.

1 participant