- Add GET /mints/{mint_id}/profile - full mint profile in one request
- Add GET /mints/cards - lightweight cards for directory/grid views
- Add GET /mints/{mint_id}/metrics - timeseries metrics for charts
- Add GET /analytics/activity - ecosystem activity timeline
- Add POST /mints/compare - side-by-side mint comparison
- Add GET /mints/rankings - ranked leaderboards by trust/uptime/latency/reviews
- Add GET /mints/{mint_id}/similar - find similar mints
- Add GET /mints/recommended - use-case based recommendations
- Add GET /mints/{mint_id}/compatibility - wallet compatibility info
- Add GET /wallets/{wallet_name}/recommended-mints - wallet-specific recommendations
- Add GET /mints/{mint_id}/risk - risk assessment and flags
- Update OpenAPI documentation for all new endpoints
- Fix review rating parsing from content field
- Fix trust score calculation to include parsed ratings
1105 lines
32 KiB
JavaScript
1105 lines
32 KiB
JavaScript
/**
|
|
* Mint Routes
|
|
*
|
|
* Core mint discovery and management endpoints.
|
|
*
|
|
* IMPORTANT: Route order matters!
|
|
* Static routes (like /by-url/*) must come BEFORE parameterized routes (/:mint_id/*)
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
import {
|
|
getMintById,
|
|
getMintByUrl,
|
|
getMints,
|
|
getMintUrls,
|
|
submitMint,
|
|
countMintsByStatus,
|
|
resolveMint
|
|
} from '../services/MintService.js';
|
|
import { getMetadataSnapshot, getMetadataHistory, deriveFeatures } from '../services/MetadataService.js';
|
|
import { getUptime, getUptimeTimeseries, getUptimeSummary, getLatencyTimeseries } from '../services/UptimeService.js';
|
|
import { getIncidents, countIncidents, getLatestProbe } from '../services/ProbeService.js';
|
|
import { getTrustScore, getTrustScoreHistory, getTrustComparison, getTrustScoreHistoryDetailed } from '../services/TrustService.js';
|
|
import { getReviews, getReviewSummary } from '../services/ReviewService.js';
|
|
import { getPageviewStats, recordPageview, getTrendingMints, getViewsTimeseries } from '../services/PageviewService.js';
|
|
import { daysAgo } from '../utils/time.js';
|
|
import { getAllReviews, countReviews } from '../services/ReviewService.js';
|
|
import {
|
|
getMintActivity,
|
|
getRecentMints,
|
|
getUpdatedMints,
|
|
getPopularMints,
|
|
getMintStats,
|
|
getMintAvailability,
|
|
getMintCard,
|
|
getMintProfile,
|
|
getMintCards,
|
|
getMintMetrics,
|
|
compareMints,
|
|
getMintRankings,
|
|
getSimilarMints,
|
|
getRecommendedMints,
|
|
getMintCompatibility,
|
|
getWalletRecommendedMints,
|
|
getMintRisk
|
|
} from '../services/AnalyticsService.js';
|
|
|
|
const router = Router();
|
|
|
|
// ==========================================
|
|
// HELPER: Resolve mint by URL query param
|
|
// ==========================================
|
|
|
|
function getMintFromUrl(req, res) {
|
|
const { url } = req.query;
|
|
if (!url) {
|
|
res.status(400).json({ error: 'URL parameter required' });
|
|
return null;
|
|
}
|
|
|
|
const mint = getMintByUrl(url);
|
|
if (!mint) {
|
|
res.status(404).json({ error: 'Mint not found' });
|
|
return null;
|
|
}
|
|
|
|
return mint;
|
|
}
|
|
|
|
// ==========================================
|
|
// HELPER: Resolve mint middleware for ID routes
|
|
// ==========================================
|
|
|
|
function resolveMintMiddleware(req, res, next) {
|
|
const { mint_id } = req.params;
|
|
|
|
if (!mint_id) {
|
|
return res.status(400).json({ error: 'Mint ID required' });
|
|
}
|
|
|
|
const mint = resolveMint(mint_id);
|
|
if (!mint) {
|
|
return res.status(404).json({ error: 'Mint not found' });
|
|
}
|
|
|
|
req.mint = mint;
|
|
next();
|
|
}
|
|
|
|
// ==========================================
|
|
// LIST & SUBMIT (no param conflicts)
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints
|
|
* List all mints with optional filters
|
|
*/
|
|
router.get('/', (req, res) => {
|
|
try {
|
|
const { status, limit = 100, offset = 0, sort_by, sort_order } = req.query;
|
|
|
|
const mints = getMints({
|
|
status,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0,
|
|
sortBy: sort_by,
|
|
sortOrder: sort_order
|
|
});
|
|
|
|
// Enrich with uptime summary
|
|
const enrichedMints = mints.map(mint => ({
|
|
...mint,
|
|
...getUptimeSummary(mint.mint_id),
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30))
|
|
}));
|
|
|
|
res.json({
|
|
mints: enrichedMints,
|
|
total: enrichedMints.length,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error listing mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /mints/submit
|
|
*/
|
|
router.post('/submit', (req, res) => {
|
|
try {
|
|
const { mint_url } = req.body;
|
|
|
|
if (!mint_url) {
|
|
return res.status(400).json({ error: 'mint_url required' });
|
|
}
|
|
|
|
const result = submitMint(mint_url);
|
|
res.status(result.success ? 201 : 400).json(result);
|
|
} catch (error) {
|
|
console.error('[API] Error submitting mint:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// HOMEPAGE & DISCOVERY ENDPOINTS
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/activity
|
|
* Get mint ecosystem activity overview
|
|
*/
|
|
router.get('/activity', (req, res) => {
|
|
try {
|
|
const activity = getMintActivity();
|
|
res.json(activity);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint activity:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/recent
|
|
* Get recently added mints
|
|
*/
|
|
router.get('/recent', (req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 20 } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const mints = getRecentMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
|
|
res.json({
|
|
window: timeWindow,
|
|
mints
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting recent mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/updated
|
|
* Get recently updated mints
|
|
*/
|
|
router.get('/updated', (req, res) => {
|
|
try {
|
|
const { limit = 20 } = req.query;
|
|
const mints = getUpdatedMints(Math.min(parseInt(limit) || 20, 100));
|
|
res.json({ mints });
|
|
} catch (error) {
|
|
console.error('[API] Error getting updated mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/popular
|
|
* Get popular mints by views
|
|
*/
|
|
router.get('/popular', async(req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 20 } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const mints = await getPopularMints(timeWindow, Math.min(parseInt(limit) || 20, 100));
|
|
res.json({
|
|
window: timeWindow,
|
|
mints
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting popular mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/trending
|
|
* Get trending mints by view velocity
|
|
*/
|
|
router.get('/trending', async(req, res) => {
|
|
try {
|
|
const { window = '7d', limit = 10 } = req.query;
|
|
|
|
// Validate window
|
|
const validWindows = ['24h', '7d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
|
|
const trending = await getTrendingMints(
|
|
Math.min(parseInt(limit) || 10, 50),
|
|
timeWindow
|
|
);
|
|
|
|
res.json({
|
|
window: timeWindow,
|
|
mints: trending.map(m => ({
|
|
mint_id: m.mint_id,
|
|
canonical_url: m.canonical_url,
|
|
name: m.name,
|
|
icon_url: m.icon_url,
|
|
status: m.status,
|
|
trust_score: m.trust_score,
|
|
trust_level: m.trust_level,
|
|
view_count: m.view_count,
|
|
view_velocity: m.view_velocity,
|
|
unique_sessions: m.unique_sessions
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting trending mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/cards
|
|
* Get lightweight mint cards for directory/list views
|
|
*/
|
|
router.get('/cards', (req, res) => {
|
|
try {
|
|
const { status, limit = 100, offset = 0, sort_by = 'trust_score', sort_order = 'desc' } = req.query;
|
|
|
|
const cards = getMintCards({
|
|
status,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0,
|
|
sortBy: sort_by,
|
|
sortOrder: sort_order
|
|
});
|
|
|
|
res.json({
|
|
cards,
|
|
total: cards.length,
|
|
limit: Math.min(parseInt(limit) || 100, 500),
|
|
offset: parseInt(offset) || 0
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint cards:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /mints/compare
|
|
* Compare multiple mints side by side
|
|
*/
|
|
router.post('/compare', (req, res) => {
|
|
try {
|
|
const { mint_ids } = req.body;
|
|
|
|
if (!mint_ids || !Array.isArray(mint_ids) || mint_ids.length === 0) {
|
|
return res.status(400).json({ error: 'mint_ids array required' });
|
|
}
|
|
|
|
if (mint_ids.length > 10) {
|
|
return res.status(400).json({ error: 'Maximum 10 mints can be compared' });
|
|
}
|
|
|
|
const comparison = compareMints(mint_ids);
|
|
res.json(comparison);
|
|
} catch (error) {
|
|
console.error('[API] Error comparing mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/rankings
|
|
* Get ranked mint leaderboard
|
|
*/
|
|
router.get('/rankings', (req, res) => {
|
|
try {
|
|
const { by = 'trust', period = '30d', limit = 50 } = req.query;
|
|
|
|
const validBy = ['trust', 'uptime', 'latency', 'reviews'];
|
|
const validPeriods = ['7d', '30d', '90d'];
|
|
|
|
const rankings = getMintRankings(
|
|
validBy.includes(by) ? by : 'trust',
|
|
validPeriods.includes(period) ? period : '30d',
|
|
Math.min(parseInt(limit) || 50, 100)
|
|
);
|
|
|
|
res.json(rankings);
|
|
} catch (error) {
|
|
console.error('[API] Error getting rankings:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/recommended
|
|
* Get recommended mints by use case
|
|
*/
|
|
router.get('/recommended', (req, res) => {
|
|
try {
|
|
const { use_case = 'general', limit = 10 } = req.query;
|
|
|
|
const validUseCases = ['general', 'mobile', 'high_volume', 'privacy'];
|
|
const recommendations = getRecommendedMints(
|
|
validUseCases.includes(use_case) ? use_case : 'general',
|
|
Math.min(parseInt(limit) || 10, 50)
|
|
);
|
|
|
|
res.json(recommendations);
|
|
} catch (error) {
|
|
console.error('[API] Error getting recommendations:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// BY-URL ROUTES (must come BEFORE :mint_id routes)
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/by-url
|
|
*/
|
|
router.get('/by-url', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const uptime = getUptimeSummary(mint.mint_id);
|
|
res.json({
|
|
mint_id: mint.mint_id,
|
|
canonical_url: mint.canonical_url,
|
|
urls: getMintUrls(mint.mint_id).map(u => u.url),
|
|
name: mint.name,
|
|
icon_url: mint.icon_url,
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_success_at: mint.last_success_at,
|
|
last_failure_at: mint.last_failure_at,
|
|
...uptime,
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
|
|
trust_score: mint.trust_score,
|
|
trust_level: mint.trust_level
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint by URL:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/urls
|
|
*/
|
|
router.get('/by-url/urls', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const urls = getMintUrls(mint.mint_id);
|
|
res.json({
|
|
canonical_url: mint.canonical_url,
|
|
urls
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint URLs:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/metadata
|
|
*/
|
|
router.get('/by-url/metadata', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const metadata = getMetadataSnapshot(mint.mint_id);
|
|
if (!metadata) {
|
|
return res.json({ error: 'No metadata available', mint_id: mint.mint_id });
|
|
}
|
|
|
|
res.json({
|
|
name: metadata.name,
|
|
pubkey: metadata.pubkey,
|
|
version: metadata.version,
|
|
description: metadata.description,
|
|
description_long: metadata.description_long,
|
|
contact: metadata.contact,
|
|
motd: metadata.motd,
|
|
icon_url: metadata.icon_url,
|
|
urls: metadata.urls,
|
|
tos_url: metadata.tos_url,
|
|
nuts: metadata.nuts,
|
|
server_time: metadata.server_time,
|
|
last_fetched_at: metadata.last_fetched_at
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/metadata/history
|
|
*/
|
|
router.get('/by-url/metadata/history', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50 } = req.query;
|
|
const history = getMetadataHistory(mint.mint_id, { limit: parseInt(limit) });
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/status
|
|
*/
|
|
router.get('/by-url/status', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const latestProbe = getLatestProbe(mint.mint_id);
|
|
res.json({
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_checked_at: latestProbe ? latestProbe.probed_at : null,
|
|
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting status:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/uptime
|
|
*/
|
|
router.get('/by-url/uptime', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { window = '24h' } = req.query;
|
|
const uptime = getUptime(mint.mint_id, window);
|
|
if (!uptime) {
|
|
return res.json({ error: 'No uptime data available' });
|
|
}
|
|
|
|
res.json(uptime);
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/uptime/timeseries
|
|
*/
|
|
router.get('/by-url/uptime/timeseries', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const timeseries = getUptimeTimeseries(mint.mint_id, window, bucket);
|
|
res.json({ data: timeseries });
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/incidents
|
|
*/
|
|
router.get('/by-url/incidents', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50 } = req.query;
|
|
const incidents = getIncidents(mint.mint_id, { limit: parseInt(limit) });
|
|
res.json({ incidents });
|
|
} catch (error) {
|
|
console.error('[API] Error getting incidents:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/trust
|
|
*/
|
|
router.get('/by-url/trust', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const trust = getTrustScore(mint.mint_id);
|
|
if (!trust) {
|
|
return res.json({
|
|
score_total: null,
|
|
score_level: 'unknown',
|
|
breakdown: null,
|
|
computed_at: null
|
|
});
|
|
}
|
|
|
|
res.json(trust);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust score:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/reviews
|
|
*/
|
|
router.get('/by-url/reviews', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
const reviews = getReviews(mint.mint_id, {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({ reviews });
|
|
} catch (error) {
|
|
console.error('[API] Error getting reviews:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/views
|
|
*/
|
|
router.get('/by-url/views', async(req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const stats = await getPageviewStats(mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting views:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/by-url/features
|
|
*/
|
|
router.get('/by-url/features', (req, res) => {
|
|
try {
|
|
const mint = getMintFromUrl(req, res);
|
|
if (!mint) return;
|
|
|
|
const features = deriveFeatures(mint.mint_id);
|
|
res.json(features);
|
|
} catch (error) {
|
|
console.error('[API] Error getting features:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// PARAMETERIZED ROUTES (:mint_id)
|
|
// These must come AFTER /by-url/* routes
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/:mint_id
|
|
*/
|
|
router.get('/:mint_id', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { mint } = req;
|
|
|
|
// Enrich with computed fields
|
|
const uptime = getUptimeSummary(mint.mint_id);
|
|
const response = {
|
|
mint_id: mint.mint_id,
|
|
canonical_url: mint.canonical_url,
|
|
urls: getMintUrls(mint.mint_id).map(u => u.url),
|
|
name: mint.name,
|
|
icon_url: mint.icon_url,
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_success_at: mint.last_success_at,
|
|
last_failure_at: mint.last_failure_at,
|
|
...uptime,
|
|
incidents_7d: countIncidents(mint.mint_id, daysAgo(7)),
|
|
incidents_30d: countIncidents(mint.mint_id, daysAgo(30)),
|
|
trust_score: mint.trust_score,
|
|
trust_level: mint.trust_level
|
|
};
|
|
|
|
// Record pageview
|
|
recordPageview(mint.mint_id, {
|
|
sessionId: req.headers['x-session-id'],
|
|
userAgent: req.headers['user-agent'],
|
|
referer: req.headers['referer']
|
|
});
|
|
|
|
res.json(response);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/urls
|
|
*/
|
|
router.get('/:mint_id/urls', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const urls = getMintUrls(req.mint.mint_id);
|
|
|
|
res.json({
|
|
canonical_url: req.mint.canonical_url,
|
|
urls
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint URLs:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/metadata
|
|
*/
|
|
router.get('/:mint_id/metadata', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const metadata = getMetadataSnapshot(req.mint.mint_id);
|
|
|
|
if (!metadata) {
|
|
return res.json({ error: 'No metadata available', mint_id: req.mint.mint_id });
|
|
}
|
|
|
|
res.json({
|
|
name: metadata.name,
|
|
pubkey: metadata.pubkey,
|
|
version: metadata.version,
|
|
description: metadata.description,
|
|
description_long: metadata.description_long,
|
|
contact: metadata.contact,
|
|
motd: metadata.motd,
|
|
icon_url: metadata.icon_url,
|
|
urls: metadata.urls,
|
|
tos_url: metadata.tos_url,
|
|
nuts: metadata.nuts,
|
|
server_time: metadata.server_time,
|
|
last_fetched_at: metadata.last_fetched_at
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/metadata/history
|
|
*/
|
|
router.get('/:mint_id/metadata/history', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50 } = req.query;
|
|
const history = getMetadataHistory(req.mint.mint_id, { limit: parseInt(limit) });
|
|
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting metadata history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/status
|
|
*/
|
|
router.get('/:mint_id/status', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { mint } = req;
|
|
const latestProbe = getLatestProbe(mint.mint_id);
|
|
|
|
res.json({
|
|
status: mint.status,
|
|
offline_since: mint.offline_since,
|
|
last_checked_at: latestProbe ? latestProbe.probed_at : null,
|
|
current_rtt_ms: latestProbe ? latestProbe.rtt_ms : null
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting status:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/uptime
|
|
*/
|
|
router.get('/:mint_id/uptime', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h' } = req.query;
|
|
const uptime = getUptime(req.mint.mint_id, window);
|
|
|
|
if (!uptime) {
|
|
return res.json({ error: 'No uptime data available' });
|
|
}
|
|
|
|
res.json(uptime);
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/uptime/timeseries
|
|
*/
|
|
router.get('/:mint_id/uptime/timeseries', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const timeseries = getUptimeTimeseries(req.mint.mint_id, window, bucket);
|
|
|
|
res.json({ data: timeseries });
|
|
} catch (error) {
|
|
console.error('[API] Error getting uptime timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/incidents
|
|
*/
|
|
router.get('/:mint_id/incidents', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50 } = req.query;
|
|
const incidents = getIncidents(req.mint.mint_id, { limit: parseInt(limit) });
|
|
|
|
res.json({ incidents });
|
|
} catch (error) {
|
|
console.error('[API] Error getting incidents:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust
|
|
*/
|
|
router.get('/:mint_id/trust', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const trust = getTrustScore(req.mint.mint_id);
|
|
|
|
if (!trust) {
|
|
return res.json({
|
|
score_total: null,
|
|
score_level: 'unknown',
|
|
breakdown: null,
|
|
computed_at: null
|
|
});
|
|
}
|
|
|
|
res.json(trust);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust score:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/reviews
|
|
*/
|
|
router.get('/:mint_id/reviews', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
const reviews = getReviews(req.mint.mint_id, {
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
});
|
|
|
|
res.json({ reviews });
|
|
} catch (error) {
|
|
console.error('[API] Error getting reviews:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/views
|
|
*/
|
|
router.get('/:mint_id/views', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const stats = await getPageviewStats(req.mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting views:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/features
|
|
*/
|
|
router.get('/:mint_id/features', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const features = deriveFeatures(req.mint.mint_id);
|
|
res.json(features);
|
|
} catch (error) {
|
|
console.error('[API] Error getting features:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
// ==========================================
|
|
// MINT DETAIL AGGREGATED ENDPOINTS
|
|
// ==========================================
|
|
|
|
/**
|
|
* GET /mints/:mint_id/stats
|
|
* Aggregated mint KPIs (single endpoint for summary cards)
|
|
*/
|
|
router.get('/:mint_id/stats', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const stats = await getMintStats(req.mint.mint_id);
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint stats:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/latency/timeseries
|
|
* Response time history for charting
|
|
*/
|
|
router.get('/:mint_id/latency/timeseries', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '24h', bucket = '1h' } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const validBuckets = ['5m', '15m', '1h'];
|
|
|
|
const timeWindow = validWindows.includes(window) ? window : '24h';
|
|
const timeBucket = validBuckets.includes(bucket) ? bucket : '1h';
|
|
|
|
const data = getLatencyTimeseries(req.mint.mint_id, timeWindow, timeBucket);
|
|
res.json({
|
|
window: timeWindow,
|
|
bucket: timeBucket,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting latency timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/availability
|
|
* Availability breakdown (online/degraded/offline percentages)
|
|
*/
|
|
router.get('/:mint_id/availability', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { window = '30d' } = req.query;
|
|
const validWindows = ['24h', '7d', '30d'];
|
|
const timeWindow = validWindows.includes(window) ? window : '30d';
|
|
|
|
const availability = getMintAvailability(req.mint.mint_id, timeWindow);
|
|
res.json(availability);
|
|
} catch (error) {
|
|
console.error('[API] Error getting availability:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust/history
|
|
* Trust score history with change reasons
|
|
*/
|
|
router.get('/:mint_id/trust/history', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 30 } = req.query;
|
|
const history = getTrustScoreHistoryDetailed(
|
|
req.mint.mint_id,
|
|
Math.min(parseInt(limit) || 30, 100)
|
|
);
|
|
res.json({ history });
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust history:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/trust/compare
|
|
* Compare trust score against ecosystem benchmarks
|
|
*/
|
|
router.get('/:mint_id/trust/compare', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { against = 'ecosystem' } = req.query;
|
|
const validBenchmarks = ['ecosystem', 'top10', 'median'];
|
|
const benchmark = validBenchmarks.includes(against) ? against : 'ecosystem';
|
|
|
|
const comparison = getTrustComparison(req.mint.mint_id, benchmark);
|
|
res.json(comparison);
|
|
} catch (error) {
|
|
console.error('[API] Error getting trust comparison:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/reviews/summary
|
|
* Quick review overview (rating distribution, averages)
|
|
*/
|
|
router.get('/:mint_id/reviews/summary', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const summary = getReviewSummary(req.mint.mint_id);
|
|
res.json(summary);
|
|
} catch (error) {
|
|
console.error('[API] Error getting review summary:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/views/timeseries
|
|
* Pageview history for adoption trends
|
|
*/
|
|
router.get('/:mint_id/views/timeseries', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const { window = '7d', bucket = '1d' } = req.query;
|
|
const validWindows = ['7d', '30d'];
|
|
const validBuckets = ['1h', '1d'];
|
|
|
|
const timeWindow = validWindows.includes(window) ? window : '7d';
|
|
const timeBucket = validBuckets.includes(bucket) ? bucket : '1d';
|
|
|
|
const data = await getViewsTimeseries(req.mint.mint_id, timeWindow, timeBucket);
|
|
res.json({
|
|
window: timeWindow,
|
|
bucket: timeBucket,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
console.error('[API] Error getting views timeseries:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/card
|
|
* Optimized endpoint for grid/list views
|
|
*/
|
|
router.get('/:mint_id/card', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const card = getMintCard(req.mint.mint_id);
|
|
if (!card) {
|
|
return res.status(404).json({ error: 'Mint not found' });
|
|
}
|
|
res.json(card);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint card:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/profile
|
|
* Full mint profile - aggregated data in one request
|
|
*/
|
|
router.get('/:mint_id/profile', resolveMintMiddleware, async(req, res) => {
|
|
try {
|
|
const profile = await getMintProfile(req.mint.mint_id);
|
|
if (!profile) {
|
|
return res.status(404).json({ error: 'Mint not found' });
|
|
}
|
|
|
|
// Record pageview
|
|
recordPageview(req.mint.mint_id, {
|
|
sessionId: req.headers['x-session-id'],
|
|
userAgent: req.headers['user-agent'],
|
|
referer: req.headers['referer']
|
|
});
|
|
|
|
res.json(profile);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint profile:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/metrics
|
|
* Mint metrics timeseries for charts
|
|
*/
|
|
router.get('/:mint_id/metrics', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { range = '7d' } = req.query;
|
|
const validRanges = ['7d', '30d', '90d'];
|
|
const timeRange = validRanges.includes(range) ? range : '7d';
|
|
|
|
const metrics = getMintMetrics(req.mint.mint_id, timeRange);
|
|
res.json(metrics);
|
|
} catch (error) {
|
|
console.error('[API] Error getting mint metrics:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/similar
|
|
* Find similar mints
|
|
*/
|
|
router.get('/:mint_id/similar', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const { limit = 5 } = req.query;
|
|
const similar = getSimilarMints(
|
|
req.mint.mint_id,
|
|
Math.min(parseInt(limit) || 5, 20)
|
|
);
|
|
res.json(similar);
|
|
} catch (error) {
|
|
console.error('[API] Error getting similar mints:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/compatibility
|
|
* Wallet compatibility info
|
|
*/
|
|
router.get('/:mint_id/compatibility', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const compatibility = getMintCompatibility(req.mint.mint_id);
|
|
res.json(compatibility);
|
|
} catch (error) {
|
|
console.error('[API] Error getting compatibility:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /mints/:mint_id/risk
|
|
* Risk assessment and flags
|
|
*/
|
|
router.get('/:mint_id/risk', resolveMintMiddleware, (req, res) => {
|
|
try {
|
|
const risk = getMintRisk(req.mint.mint_id);
|
|
res.json(risk);
|
|
} catch (error) {
|
|
console.error('[API] Error getting risk assessment:', error);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router; |