-
Notifications
You must be signed in to change notification settings - Fork 23
stellar: validate signed XDR contents before submit; store expectedHa… #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export default { | ||
| testEnvironment: "node", | ||
| transform: {}, | ||
| setupFilesAfterEnv: ["./test/jest.setup.js"], | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { | |
| submitTransaction, | ||
| verifyTransaction, | ||
| verifyPaymentOperations, | ||
| validateSignedPaymentXdr, | ||
| findPaymentPaths, | ||
| applySlippage, | ||
| NETWORK, | ||
|
|
@@ -333,7 +334,8 @@ export const initializePayment = async (req, res) => { | |
| network: NETWORK, | ||
| status: "pending", | ||
| settlement: settlementMode, | ||
| stellarTxHash: paymentTx.hash, | ||
| expectedHash: paymentTx.hash, | ||
| memo, | ||
| ...(sendAssetInput && { | ||
| sendAsset: sendAssetInput, | ||
| sendMax, | ||
|
|
@@ -428,7 +430,51 @@ export const submitPayment = async (req, res) => { | |
| }); | ||
| } | ||
|
|
||
| // Update status to submitted | ||
| // 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 | ||
|
Comment on lines
+433
to
+477
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major Path payments will always fail this pre-submission check.
🤖 Prompt for AI Agents |
||
| transaction.status = "submitted"; | ||
| transaction.submittedAt = new Date(); | ||
| await transaction.save({ session }); | ||
|
|
@@ -457,23 +503,7 @@ export const submitPayment = async (req, res) => { | |
|
|
||
| // Verify on-chain that the creator (and platform, when a fee was applied) | ||
| // actually received the expected USDC amounts | ||
| const 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, | ||
| }, | ||
| ]; | ||
| // (expectedPayments already defined above for pre-submission validation) | ||
|
|
||
| const verification = await verifyPaymentOperations( | ||
| result.hash, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -386,6 +386,71 @@ export const submitTransaction = async (signedXdr) => { | |
| } | ||
| }; | ||
|
|
||
| /** | ||
| * 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; | ||
| }; | ||
|
|
||
|
Comment on lines
+389
to
+453
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.jsRepository: Deen-Bridge/dnb-backend Length of output: 10474
🤖 Prompt for AI AgentsSource: Path instructions |
||
| export const verifyTransaction = async (txHash) => { | ||
| try { | ||
| const tx = await timedHorizonCall("fetchTransaction", () => | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { MongoMemoryServer } from "mongodb-memory-server"; | ||
|
|
||
| let mongod; | ||
|
|
||
| beforeAll(async () => { | ||
| // Start in-memory MongoDB | ||
| mongod = await MongoMemoryServer.create(); | ||
| const mongoUri = mongod.getUri(); | ||
|
|
||
| // Set test environment and MongoDB URI | ||
| process.env.NODE_ENV = "test"; | ||
| process.env.MONGO_URI = mongoUri; | ||
| process.env.JWT_SECRET = process.env.JWT_SECRET || "test-secret-key-at-least-32-characters-long"; | ||
| process.env.PORT = process.env.PORT || "5000"; | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| if (mongod) { | ||
| await mongod.stop(); | ||
| } | ||
| }); | ||
|
|
There was a problem hiding this comment.
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 beforebeforeAllpublishes 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
Source: Path instructions