Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9a992aa
feat(soroban): on-chain contribution router with slippage enforcement…
cybermax4200 Apr 29, 2026
adbe262
fix(ci): add eslint.config.js for ESLint v9+ flat config in backend a…
cybermax4200 Apr 29, 2026
46eb13a
fix(ci): remove @eslint/js import — use zero-dependency flat config
cybermax4200 Apr 29, 2026
8422121
fix(ci): use CJS module.exports in eslint.config; remove stray commen…
cybermax4200 Apr 29, 2026
d04794c
fix(ci): exclude JSX files from ESLint — no JSX parser installed
cybermax4200 Apr 29, 2026
369e9fd
fix(sorobanService): remove unused logger and Vec imports
cybermax4200 Apr 29, 2026
eeba093
fix: remove duplicate rows declaration in campaigns.js and close miss…
cybermax4200 Apr 30, 2026
fd62d32
fix: close missing route handler brace in campaigns.js and remove dup…
cybermax4200 Apr 30, 2026
4d911df
fix: guard USDC Asset construction against null USDC_ISSUER in test e…
cybermax4200 Apr 30, 2026
2c121bb
fix: guard PLATFORM_KEYPAIR construction against undefined PLATFORM_S…
cybermax4200 Apr 30, 2026
666aeb4
fix: guard PLATFORM_PUBLIC_KEY export against null PLATFORM_KEYPAIR
cybermax4200 Apr 30, 2026
28d3a50
fix(tests): use valid UUIDs for campaign_id, valid Stellar key for de…
cybermax4200 Apr 30, 2026
623e57e
fix(tests): mock validation middleware in unit tests and fix stale qu…
cybermax4200 Apr 30, 2026
4cdf8a3
fix: close missing ternary and div in Campaign.jsx report button block
cybermax4200 Apr 30, 2026
08c6e43
fix: restore missing api.prepareContribution call in submitWithFreighter
cybermax4200 Apr 30, 2026
62c1f61
fix: implement on-chain DEX swap in router, add DB timeout, fix CI hang
cybermax4200 Apr 30, 2026
92557e0
fix: add --test-force-exit to prevent CI hang after test suite completes
cybermax4200 Apr 30, 2026
cf5f5a1
fix: resolve 8 failing tests - submit-signed handler, error propagati…
cybermax4200 Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-smt-pass-here

# Soroban contribution router contract (enables on-chain slippage enforcement + atomic fee split)
# Leave unset to fall back to classic path payment
ROUTER_CONTRACT_ID=
XLM_SAC_ADDRESS=
USDC_SAC_ADDRESS=
19 changes: 19 additions & 0 deletions backend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint.config.js — CommonJS flat config (no "type":"module" in package.json)
module.exports = [
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'commonjs',
globals: {
require: 'readonly', module: 'readonly', exports: 'readonly', __dirname: 'readonly',
process: 'readonly', Buffer: 'readonly', console: 'readonly',
setTimeout: 'readonly', setInterval: 'readonly', setImmediate: 'readonly',
fetch: 'readonly', URL: 'readonly', URLSearchParams: 'readonly', AbortSignal: 'readonly',
},
},
rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
},
{ ignores: ['node_modules/', 'dist/'] },
];
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"test": "JWT_SECRET=testsecret node --test src/**/*.test.js",
"test": "JWT_SECRET=testsecret node --test --test-force-exit src/**/*.test.js",
"rotate-wallet-secrets": "node src/scripts/rotateWalletSecrets.js"
},
"dependencies": {
Expand Down
5 changes: 4 additions & 1 deletion backend/src/config/database.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { Pool } = require('pg');
const logger = require('./logger');

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
connectionTimeoutMillis: 5000,
});

pool.on('error', (err) => {
logger.error('Unexpected database pool error', { error: err.message });
Expand Down
2 changes: 1 addition & 1 deletion backend/src/config/stellar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const server = new Horizon.Server(
const networkPassphrase = isTestnet ? Networks.TESTNET : Networks.PUBLIC;

// USDC asset — issuer differs between testnet and mainnet
const USDC = new Asset('USDC', process.env.USDC_ISSUER);
const USDC = process.env.USDC_ISSUER ? new Asset('USDC', process.env.USDC_ISSUER) : null;

function parseAdditionalAssets() {
if (!process.env.STELLAR_EXTRA_ASSETS) return {};
Expand Down
1 change: 1 addition & 0 deletions backend/src/models.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('Database Models & Constraints', async () => {
await client.query('ROLLBACK');
client.release();
}
await pool.end();
});

it('should allow creating a valid user', async () => {
Expand Down
8 changes: 1 addition & 7 deletions backend/src/routes/campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
getSupportedAssetCodes,
buildWithdrawalTransaction,
} = require('../services/stellarService');
const { encryptSecret } = require('../services/walletService');

Check warning on line 11 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'encryptSecret' is assigned a value but never used
const { watchCampaignWallet, addSSEClient, removeSSEClient } = require('../services/ledgerMonitor');
const { invokeContract, encodeMilestone, nativeToScVal } = require('../services/sorobanService');

Check warning on line 13 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'nativeToScVal' is assigned a value but never used

Check warning on line 13 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'encodeMilestone' is assigned a value but never used

Check warning on line 13 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'invokeContract' is assigned a value but never used
const { insertWithdrawalPendingSignatures } = require('../services/stellarTransactionService');
const { sendEmail } = require('../services/emailService');
const { uploadCampaignCoverImage } = require('../services/storage');
const { isKycRequiredForCampaigns } = require('../services/kycProvider');
const {
createCampaignValidation,

Check warning on line 19 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'createCampaignValidation' is assigned a value but never used
createCampaignUpdateValidation,
getCampaignsValidation,
validateRequest,
Expand Down Expand Up @@ -191,13 +191,6 @@

// Get single Campaign
router.get('/:id', async (req, res) => {
const { rows } = await db.query(
`SELECT c.*, u.name AS creator_name, u.kyc_status AS creator_kyc_status
FROM campaigns c
JOIN users u ON u.id = c.creator_id
WHERE c.id = $1`,
[req.params.id]
);
const query = `
SELECT *,
(SELECT COUNT(DISTINCT sender_public_key)::int FROM contributions WHERE campaign_id = $1) AS contributor_count
Expand Down Expand Up @@ -232,7 +225,7 @@
}
}
}
} catch (err) {

Check warning on line 228 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'err' is defined but never used
// Ignore invalid token for public route
}
}
Expand Down Expand Up @@ -274,6 +267,7 @@
progress_percentage: Math.round(pct * 10) / 10,
contribution_url: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/campaigns/${campaign.id}`,
});
});
// Get backers for a campaign
router.get('/:id/backers', async (req, res) => {
const campaignId = req.params.id;
Expand Down Expand Up @@ -607,7 +601,7 @@
[coverImageUrl, req.params.id]
);
res.json(updatedRows[0]);
} catch (err) {

Check warning on line 604 in backend/src/routes/campaigns.js

View workflow job for this annotation

GitHub Actions / backend-checks

'err' is defined but never used
return res.status(500).json({ error: 'Could not upload campaign cover image' });
}
}
Expand Down
10 changes: 8 additions & 2 deletions backend/src/routes/campaigns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
next();
},
},
'../middleware/validation': {
createCampaignValidation: [],
getCampaignsValidation: [],
createCampaignUpdateValidation: [],
validateRequest: (_req, _res, next) => next(),
},
});

const app = express();
Expand Down Expand Up @@ -73,7 +79,7 @@
const app = buildApp({
authUser: { userId: 'creator-1', role: 'creator' },
queryImpl: async (text) => {
if (text.includes('SELECT wallet_public_key, kyc_status FROM users')) {
if (text.includes('SELECT email, wallet_public_key, kyc_status FROM users')) {
return { rows: [{ wallet_public_key: 'GCREATOR', kyc_status: 'pending' }] };
}
return { rows: [] };
Expand Down Expand Up @@ -102,7 +108,7 @@
const app = buildApp({
authUser: { userId: 'creator-1', role: 'creator' },
queryImpl: async (text) => {
if (text.includes('SELECT wallet_public_key, kyc_status FROM users')) {
if (text.includes('SELECT email, wallet_public_key, kyc_status FROM users')) {
return { rows: [{ wallet_public_key: 'GCREATOR', kyc_status: 'unverified' }] };
}
if (text.includes('INSERT INTO campaigns')) {
Expand Down Expand Up @@ -134,7 +140,7 @@

test('POST /api/campaigns/:id/trigger-refunds creates refund requests for contributions', async () => {
const created = [];
const queryImpl = async (text, params) => {

Check warning on line 143 in backend/src/routes/campaigns.test.js

View workflow job for this annotation

GitHub Actions / backend-checks

'params' is defined but never used. Allowed unused args must match /^_/u
if (text.includes('SELECT id, wallet_public_key, status FROM campaigns')) {
return { rows: [{ id: 'c-1', wallet_public_key: 'GPK', status: 'failed' }] };
}
Expand Down
68 changes: 9 additions & 59 deletions backend/src/routes/contributions.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,42 +314,12 @@ router.post('/submit-signed', requireAuth, async (req, res) => {
return res.status(400).json({ error: 'signed_xdr and prepare_token are required' });
}

let txHash;
let conversionQuote = null;
let unsignedXdr;
let signedXdr;
let flowMetadata;
let platformFeeAmount = 0;

if (send_asset === campaign.asset_type) {
const prepared = await prepareSignedContributionPayment({
senderSecret,
destinationPublicKey: campaign.wallet_public_key,
asset: send_asset,
amount,
memo: `cp-${campaign_id}`,
});
unsignedXdr = prepared.unsignedXdr;
signedXdr = prepared.signedXdr;
platformFeeAmount = prepared.feeAmount || 0;
flowMetadata = {
flow: 'payment',
send_asset,
amount: String(amount),
contributor_public_key: contributorPublicKey,
platform_fee_amount: platformFeeAmount,
};
} else {
const paths = await getPathPaymentQuote({
sendAsset: send_asset,
destAsset: campaign.asset_type,
destAmount: amount,
});
if (!paths.length) {
return res.status(422).json({
error: `No conversion path found for ${send_asset} -> ${campaign.asset_type}`,
});
}
let prepared;
try {
prepared = verifyPreparedContributionToken(prepare_token);
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired prepare token' });
}

if (prepared.user_id !== req.user.userId) {
return res.status(403).json({ error: 'Prepared contribution token does not belong to this user' });
Expand All @@ -361,27 +331,6 @@ router.post('/submit-signed', requireAuth, async (req, res) => {
unsignedXdr: prepared.unsigned_xdr,
senderPublicKey: prepared.sender_public_key,
});
unsignedXdr = prepared.unsignedXdr;
signedXdr = prepared.signedXdr;
platformFeeAmount = prepared.feeAmount || 0;

conversionQuote = {
send_asset,
campaign_asset: campaign.asset_type,
campaign_amount: String(amount),
quoted_source_amount: bestPath.source_amount,
max_send_amount: sendMax,
path: bestPath.path,
};
flowMetadata = {
flow: 'path_payment_strict_receive',
send_asset,
dest_asset: campaign.asset_type,
dest_amount: String(amount),
max_send_amount: sendMax,
contributor_public_key: contributorPublicKey,
platform_fee_amount: platformFeeAmount,
};
} catch (err) {
return res.status(422).json({ error: err.message });
}
Expand Down Expand Up @@ -417,8 +366,8 @@ router.post('/submit-signed', requireAuth, async (req, res) => {
tx_hash: txHash,
stellar_transaction_id: stellarTransactionId,
message: 'Transaction submitted',
platform_fee_amount: platformFeeAmount,
conversion_quote: conversionQuote,
platform_fee_amount: prepared.flow_metadata?.platform_fee_amount || 0,
conversion_quote: prepared.conversion_quote || null,
});
});

Expand Down Expand Up @@ -466,6 +415,7 @@ router.post('/', requireAuth, contributionValidation, validateRequest, async (re
tx_hash: result.txHash,
stellar_transaction_id: result.stellarTransactionId,
message: 'Transaction submitted',
platform_fee_amount: result.flowMetadata?.platform_fee_amount || 0,
conversion_quote: result.conversionQuote,
});
} catch (err) {
Expand Down
Loading
Loading