Add shared LNURL types, indexing helpers, and warnings.

Introduce lnurl as a first-class mint type with probe/announcement fields
and shared helpers the API and web can both rely on.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:27 +02:00
co-authored by Cursor
parent 36c01861f5
commit c97b44018d
8 changed files with 1553 additions and 14 deletions
+112 -5
View File
@@ -18,7 +18,7 @@ export type MintStatus = 'online' | 'degraded' | 'offline' | 'unknown' | 'announ
* has to be able to hold a value it does not have a page for. Compare against
* `ANNOUNCEMENT_KINDS` rather than switching exhaustively on this.
*/
export type MintType = 'cashu' | 'fedimint' | (string & {});
export type MintType = 'cashu' | 'fedimint' | 'lnurl' | (string & {});
/** One item of `GET /api/mints`. */
export interface MintListItem {
@@ -26,7 +26,7 @@ export interface MintListItem {
host: string;
name: string | null;
icon: string | null;
/** 'cashu' or 'fedimint'. Rows written before the column existed read as 'cashu'. */
/** 'cashu', 'fedimint' or 'lnurl'. Rows written before the column existed read as 'cashu'. */
type: MintType;
status: MintStatus;
last_online: number | null;
@@ -48,10 +48,14 @@ export type RatingDistribution = Record<'1' | '2' | '3' | '4' | '5', number>;
/**
* `GET /api/mints/:host`.
*
* The Fedimint keys are optional and absent on a Cashu mint, which is what keeps the
* Cashu payload unchanged. Read them through `FedimintDetail` after checking `type`.
* The Fedimint and LNURL keys are optional and absent on a Cashu mint, which is what
* keeps the Cashu payload unchanged. Read them through `FedimintDetail` or
* `LnurlDetail` after checking `type`.
*/
export interface MintDetail extends MintListItem, Partial<FedimintFields> {
export interface MintDetail
extends MintListItem,
Partial<FedimintFields>,
Partial<LnurlFields> {
description: string | null;
pubkey: string | null;
info: MintInfo | null;
@@ -100,6 +104,97 @@ export interface FedimintFields {
/** `GET /api/mints/:host` for a Fedimint federation: the detail plus its own fields. */
export type FedimintDetail = MintDetail & FedimintFields;
/**
* What `ecosystem_json` holds for an LNURL row.
*
* Two sources, never mixed: `features` is what the operator *announced* on Nostr, and
* everything under "probed" is what the mint's own endpoints said when they were last
* reached. The kind document makes that separation normative — a prober never rewrites
* an operator's capability list — and keeping the two in different fields is what makes
* it impossible to do by accident.
*
* Every millisatoshi field is stored exactly as the wire gave it. Conversion to sats
* happens once, at render, through `msatToSat`.
*/
export interface LnurlFields {
/** The `d` tag: the mint pubkey when it has one, else the normalized host. */
lnurl_id: string;
/** The https base URL. `host` is the routing slug derived from it. */
base_url: string;
/** The `features` tag, split. The operator's claim, never edited by a probe. */
features: string[];
/**
* What the last probe actually observed this mint serving.
*
* Kept apart from `features` above rather than merged into it, because the two are
* different kinds of statement — a claim and an observation — and the kind document
* makes it normative that a prober never rewrites the first. The page renders their
* union through `displayFeatures`; the publisher signs only this one.
*/
observed_features: string[];
/** The `n` tag, normalized — `bitcoin` and `mainnet` both arrive as `mainnet`. */
network: string | null;
/** `created_at` of the newest announcement seen. null for a seeded row. */
announced_at: number | null;
/** Who published that announcement. The `a` tag of a review points back at them. */
announcer_pubkey: string | null;
/* ---- probed: from the mint's own endpoints ---- */
/**
* The funding node's identity key, from the mint advertisement.
*
* Sticky once learned: a probe that finds none does not clear it, because a node
* being unreachable for one request is not a change of identity. Its *absence from
* the latest probe* is recorded separately, in `funding_available`.
*/
mint_pubkey: string | null;
/**
* Whether the last probe found a reachable funding source.
*
* null before anything has probed. false is the degraded-but-online state: the mint
* answers, its limits are real, and `rotate`/`split`/`merge` still work, but nothing
* moves in or out over Lightning. See NOTES-LNURL.md for why this one bit cannot
* distinguish "never configured" from "unreachable right now", and why that is fine.
*/
funding_available: boolean | null;
/** Which endpoint answered: the withdraw advertisement, or the payRequest fallback. */
probe_endpoint: string | null;
/**
* Set when the host answered but with something that is not a mint advertisement.
*
* A distinct outcome from both online and offline, and it has to be: these endpoints
* return HTTP 200 for their errors, so "responding" and "working" are different
* questions. Carries the short reason, for the banner.
*/
invalid_reason: string | null;
/** Withdraw bounds, millisatoshi: what a note's value can actually be. */
min_withdrawable_msat: number | null;
max_withdrawable_msat: number | null;
/** Pay bounds, millisatoshi: what a minter can actually send. Not the same numbers. */
min_sendable_msat: number | null;
max_sendable_msat: number | null;
/** `Mint fees: <base>,<ppm>` from the payRequest metadata. Absent means fee-free. */
fee_base_msat: number | null;
fee_ppm: number | null;
/** The LUD-16 address, derived from `payLink` rather than the echoed identifier. */
lightning_address: string | null;
/** The Tor address from the one-pager, when one is advertised. */
onion_url: string | null;
/** The funding node, as the mint chooses to describe it. All optional, all msat. */
node_alias: string | null;
node_uri: string | null;
node_capacity_msat: number | null;
node_channels: number | null;
node_peers: number | null;
}
/** `GET /api/mints/:host` for an LNURL mint: the detail plus its own fields. */
export type LnurlDetail = MintDetail & LnurlFields;
/**
* `GET /api/stats`.
*
@@ -127,6 +222,18 @@ export interface Stats {
fedimint_announced: number;
/** Reviews of federations (`k` = 38173), included in `reviews_total`. */
fedimint_reviews: number;
lnurl_total: number;
/** LNURL mints a probe reached. Online includes the degraded-funding ones. */
lnurl_online: number;
lnurl_offline: number;
/**
* Online mints whose funding source was unreachable at the last probe: up and
* serving, but nothing moves in or out over Lightning. A subset of `lnurl_online`,
* never added to it.
*/
lnurl_degraded_funding: number;
/** Reviews of LNURL mints (`k` = 38174), included in `reviews_total`. */
lnurl_reviews: number;
}
/** `GET /api/health`. */