Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions backend/src/routes/generator/explorer.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Router, Request, Response } from 'express';
import {
filterTransactions,
getExplorerSnapshot,
buildExplorerLink,
} from '../../services/blockExplorer.service.js';
import logger from '../../utils/logger.js';

const router = Router();

/**
* @route GET /api/v1/generator/explorer/snapshot
* @desc Get cached ledger snapshot for hackathon research
*/
router.get('/explorer/snapshot', async (req: Request, res: Response) => {
try {
const limit = req.query.limit ? Number(req.query.limit) : 25;
const seed = req.query.seed ? Number(req.query.seed) : undefined;
const snapshot = await getExplorerSnapshot({ limit, seed });
res.json({ status: 'success', data: snapshot });
} catch (error) {
logger.error('Block explorer snapshot failed', { error });
res.status(500).json({ error: 'Failed to fetch explorer snapshot' });
}
});

/**
* @route GET /api/v1/generator/explorer/search
* @desc Filter transactions by query string
*/
router.get('/explorer/search', async (req: Request, res: Response) => {
try {
const query = String(req.query.q ?? '');
const snapshot = await getExplorerSnapshot({ limit: 50 });
const filtered = filterTransactions(snapshot.transactions, query);
res.json({
status: 'success',
data: {
transactions: filtered,
stats: snapshot.stats,
query,
},
});
} catch (error) {
logger.error('Block explorer search failed', { error });
res.status(500).json({ error: 'Failed to search transactions' });
}
});

/**
* @route GET /api/v1/generator/explorer/link/:hash
* @desc Build external explorer URL for a transaction hash
*/
router.get('/explorer/link/:hash', (req: Request, res: Response) => {
const network = req.query.network === 'public' ? 'public' : 'testnet';
const link = buildExplorerLink(req.params.hash, network);
res.json({ status: 'success', data: { link } });
});

export default router;
8 changes: 8 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import coursesRouter from './courses.js';
import enrollmentsRouter from './enrollments.js';
import exportRouter from './export.routes.js';
import generatorRouter from './generator/generator.routes.js';
import explorerRouter from './generator/explorer.routes.js';
import healthRouter from './health.routes.js';
import osctRouter from './osct/osct.routes.js';
import playgroundRouter from './playground/playground.routes.js';
import simulatorRouter from './simulator/simulator.routes.js';
import learningRoutes from './learning/learning.routes.js';
import securityRouter from './security.routes.js';
import studentsRouter from './students.js';
Expand Down Expand Up @@ -39,6 +43,10 @@ router.use('/notifications', notificationRouter);
router.use('/notifications/preferences', notificationPreferencesRouter);
router.use('/security', securityRouter);
router.use('/generator', generatorRouter);
router.use('/generator', explorerRouter);
router.use('/osct', osctRouter);
router.use('/simulator', simulatorRouter);
router.use('/playground', playgroundRouter);
router.use('/export', exportRouter);
router.use('/webhooks', webhooksRouter);
router.use('/user', userRouter);
Expand Down
31 changes: 31 additions & 0 deletions backend/src/routes/osct/osct.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Router, Request, Response } from 'express';
import { estimateGas, validateGasRequest } from '../../services/gasEstimation.service.js';
import logger from '../../utils/logger.js';

const router = Router();

/**
* @route POST /api/v1/osct/gas-estimate
* @desc Estimate Soroban gas for open-source contribution review
*/
router.post('/gas-estimate', (req: Request, res: Response) => {
try {
const validation = validateGasRequest(req.body?.sourceCode);
if (!validation.valid) {
res.status(400).json({ error: validation.error });
return;
}

const result = estimateGas({
sourceCode: req.body.sourceCode,
budgetPreset: req.body.budgetPreset,
});

res.json({ status: 'success', data: result });
} catch (error) {
logger.error('OSCT gas estimation failed', { error });
res.status(500).json({ error: 'Failed to estimate gas' });
}
});

export default router;
71 changes: 71 additions & 0 deletions backend/src/routes/playground/playground.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Router, Request, Response } from 'express';
import {
TRIAGE_SCENARIOS,
getLeaderboard,
scoreRound,
updateLeaderboard,
} from '../../services/issueTriage.service.js';
import logger from '../../utils/logger.js';

const router = Router();

/**
* @route GET /api/v1/playground/triage/scenarios
* @desc List issue triage minigame scenarios
*/
router.get('/triage/scenarios', (_req: Request, res: Response) => {
const scenarios = TRIAGE_SCENARIOS.map(({ correctPriority, correctLabels, hint, ...rest }) => ({
...rest,
// Omit answers from client payload — scoring happens server-side
}));
res.json({ status: 'success', data: { scenarios } });
});

/**
* @route POST /api/v1/playground/triage/score
* @desc Score a triage round and optionally update leaderboard
*/
router.post('/triage/score', async (req: Request, res: Response) => {
try {
const { submissions, playerId } = req.body ?? {};
if (!Array.isArray(submissions) || submissions.length === 0) {
res.status(400).json({ error: 'submissions array is required.' });
return;
}

const round = scoreRound(submissions);
let leaderboardEntry = null;

if (playerId && typeof playerId === 'string') {
leaderboardEntry = await updateLeaderboard(
playerId,
round.totalPoints,
submissions.length
);
}

res.json({
status: 'success',
data: { ...round, leaderboardEntry },
});
} catch (error) {
logger.error('Issue triage scoring failed', { error });
res.status(500).json({ error: 'Failed to score triage round' });
}
});

/**
* @route GET /api/v1/playground/triage/leaderboard
* @desc Get Redis-backed triage leaderboard
*/
router.get('/triage/leaderboard', async (_req: Request, res: Response) => {
try {
const leaderboard = await getLeaderboard();
res.json({ status: 'success', data: { leaderboard } });
} catch (error) {
logger.error('Leaderboard fetch failed', { error });
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});

export default router;
30 changes: 30 additions & 0 deletions backend/src/routes/simulator/simulator.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Router, Request, Response } from 'express';
import {
scanContractSource,
validateScanRequest,
} from '../../services/vulnerabilityScanner.service.js';
import logger from '../../utils/logger.js';

const router = Router();

/**
* @route POST /api/v1/simulator/scan
* @desc Scan contract source for security vulnerabilities
*/
router.post('/scan', (req: Request, res: Response) => {
try {
const validation = validateScanRequest(req.body?.sourceCode);
if (!validation.valid) {
res.status(400).json({ error: validation.error });
return;
}

const result = scanContractSource(req.body.sourceCode);
res.json({ status: 'success', data: result });
} catch (error) {
logger.error('Vulnerability scan failed', { error });
res.status(500).json({ error: 'Failed to scan contract' });
}
});

export default router;
124 changes: 124 additions & 0 deletions backend/src/services/blockExplorer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Block Explorer Service — Hackathon Project Idea Generator backend.
*
* Provides ledger snapshots and transaction feeds for hackathon research.
*/

import cacheService, { CACHE_KEYS } from '../cache/CacheService.js';

export type TxStatus = 'SUCCESS' | 'PENDING' | 'FAILED';

export interface ExplorerTransaction {
id: string;
hash: string;
source: string;
destination: string;
operation: string;
amount: string;
asset: string;
fee: string;
ledger: number;
status: TxStatus;
timestamp: string;
}

export interface ExplorerSnapshot {
transactions: ExplorerTransaction[];
stats: {
totalTransactions: number;
successRate: number;
averageFee: string;
latestLedger: number;
};
generatedAt: string;
}

const OPS = ['PAYMENT', 'INVOKE_HOST_FUNCTION', 'CHANGE_TRUST', 'MANAGE_OFFER', 'CREATE_ACCOUNT'];
const ASSETS = ['XLM', 'USDC', 'EURC', 'AQUA'];

function seededRandom(seed: number): () => number {
let s = seed;
return () => {
s = (s * 1664525 + 1013904223) % 4294967296;
return s / 4294967296;
};
}

function generateTransactions(count: number, seed: number, startLedger: number): ExplorerTransaction[] {
const rand = seededRandom(seed);
return Array.from({ length: count }, (_, i) => {
const status: TxStatus = rand() > 0.08 ? 'SUCCESS' : 'FAILED';
const ledger = startLedger + Math.floor(rand() * 5);
return {
id: `tx_${seed}_${i}`,
hash: `H${seed.toString(16).padStart(8, '0')}${i.toString(16).padStart(8, '0')}`,
source: `G${Math.floor(rand() * 1e10).toString(36).toUpperCase().padStart(10, '0')}`,
destination: `G${Math.floor(rand() * 1e10).toString(36).toUpperCase().padStart(10, '0')}`,
operation: OPS[Math.floor(rand() * OPS.length)],
amount: (rand() * 1000).toFixed(2),
asset: ASSETS[Math.floor(rand() * ASSETS.length)],
fee: (100 + Math.floor(rand() * 900)).toString(),
ledger,
status,
timestamp: new Date(Date.now() - i * 60_000).toISOString(),
};
});
}

function computeStats(txs: ExplorerTransaction[]): ExplorerSnapshot['stats'] {
if (txs.length === 0) {
return { totalTransactions: 0, successRate: 0, averageFee: '0', latestLedger: 0 };
}
const succeeded = txs.filter((t) => t.status === 'SUCCESS').length;
const totalFee = txs.reduce((sum, t) => sum + Number(t.fee), 0);
return {
totalTransactions: txs.length,
successRate: Math.round((succeeded / txs.length) * 100),
averageFee: (totalFee / txs.length).toFixed(0),
latestLedger: Math.max(...txs.map((t) => t.ledger)),
};
}

export function filterTransactions(
txs: ExplorerTransaction[],
query: string
): ExplorerTransaction[] {
const q = query.trim().toLowerCase();
if (!q) return txs;
return txs.filter(
(tx) =>
tx.hash.toLowerCase().includes(q) ||
tx.operation.toLowerCase().includes(q) ||
tx.source.toLowerCase().includes(q) ||
tx.destination.toLowerCase().includes(q) ||
tx.asset.toLowerCase().includes(q)
);
}

export async function getExplorerSnapshot(options: {
limit?: number;
seed?: number;
cacheTtl?: number;
} = {}): Promise<ExplorerSnapshot> {
const limit = Math.min(options.limit ?? 25, 100);
const seed = options.seed ?? Math.floor(Date.now() / 60_000);
const cacheKey = `hackathon:explorer:${seed}:${limit}`;

const cached = await cacheService.get<ExplorerSnapshot>(cacheKey);
if (cached) return cached;

const transactions = generateTransactions(limit, seed, 524_000);
const snapshot: ExplorerSnapshot = {
transactions,
stats: computeStats(transactions),
generatedAt: new Date().toISOString(),
};

await cacheService.set(cacheKey, snapshot, options.cacheTtl ?? 120);
return snapshot;
}

export function buildExplorerLink(hash: string, network: 'testnet' | 'public' = 'testnet'): string {
const segment = network === 'public' ? 'public' : 'testnet';
return `https://stellar.expert/explorer/${segment}/tx/${hash}`;
}
Loading
Loading