Index, probe, and announce LNURL mints in the API.

Wire discovery and probing for LNURL mints, add rate-limited POST /api/index
for user submissions, and optionally announce confirmed state to relays.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:35 +02:00
co-authored by Cursor
parent c97b44018d
commit 2a9444942b
19 changed files with 4383 additions and 72 deletions
+38 -2
View File
@@ -11,6 +11,7 @@ import {
type MintType,
type ProbeSample,
type RatingDistribution,
type LnurlFields,
type Stats,
} from '@cashumints/shared';
import { config, startedAt } from './config.ts';
@@ -289,7 +290,8 @@ export async function getStats(): Promise<Stats> {
* COUNT(*) FILTER, not SUM(status = 'online'): Postgres has no implicit cast from
* boolean to integer, so the SQLite spelling is a type error there.
*/
const [counts, federations, reviews, fedimintReviews] = await Promise.all([
const [counts, federations, lnurl, lnurlFunding, reviews, fedimintReviews, lnurlReviews] =
await Promise.all([
db.get<{ total: number; online: number; offline: number; degraded: number }>(
`SELECT
COUNT(*) AS total,
@@ -306,6 +308,24 @@ export async function getStats(): Promise<Stats> {
COUNT(*) FILTER (WHERE status = 'announced') AS announced
FROM mints WHERE type = 'fedimint'`,
),
db.get<{ total: number; online: number; offline: number }>(
`SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'online') AS online,
COUNT(*) FILTER (WHERE status = 'offline') AS offline
FROM mints WHERE type = 'lnurl'`,
),
/*
* The degraded-funding count is read in JavaScript rather than in SQL, because the
* flag lives inside `ecosystem_json` and neither a `LIKE '%"funding_available":false%'`
* (which depends on how the two drivers happen to serialise) nor a JSON operator
* (which SQLite and Postgres spell differently) is portable. There are a handful of
* these rows, the whole result is memoised for a minute, and a correct answer on
* both backends is worth one small scan.
*/
db.all<{ ecosystem_json: string | null }>(
`SELECT ecosystem_json FROM mints WHERE type = 'lnurl' AND status = 'online'`,
),
db.get<{ n: number; last: number | null }>(
`WITH latest AS (${LATEST_REVIEWS}) SELECT COUNT(*) AS n, MAX(created_at) AS last FROM latest`,
),
@@ -314,7 +334,18 @@ export async function getStats(): Promise<Stats> {
SELECT COUNT(*) AS n FROM latest
WHERE mint_url IN (SELECT url FROM mints WHERE type = 'fedimint')`,
),
]);
db.get<{ n: number }>(
`WITH latest AS (${LATEST_REVIEWS})
SELECT COUNT(*) AS n FROM latest
WHERE mint_url IN (SELECT url FROM mints WHERE type = 'lnurl')`,
),
]);
let lnurlDegradedFunding = 0;
for (const row of lnurlFunding) {
const fields = parseEcosystem<LnurlFields>(row);
if (fields?.funding_available === false) lnurlDegradedFunding++;
}
const cashuTotal = counts?.total ?? 0;
@@ -332,6 +363,11 @@ export async function getStats(): Promise<Stats> {
fedimint_offline: federations?.offline ?? 0,
fedimint_announced: federations?.announced ?? 0,
fedimint_reviews: fedimintReviews?.n ?? 0,
lnurl_total: lnurl?.total ?? 0,
lnurl_online: lnurl?.online ?? 0,
lnurl_offline: lnurl?.offline ?? 0,
lnurl_degraded_funding: lnurlDegradedFunding,
lnurl_reviews: lnurlReviews?.n ?? 0,
};
statsCache = { at: now, value };