Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
187 lines
6.1 KiB
TypeScript
187 lines
6.1 KiB
TypeScript
/** Shapes returned by the API. The web app builds against these. */
|
|
|
|
/**
|
|
* Statuses a listed thing can be in.
|
|
*
|
|
* `announced` is the Fedimint addition, and it is deliberately not a synonym for
|
|
* `unknown`. `unknown` means "we have not checked yet"; `announced` means "there is no
|
|
* check we can run" — the federation exists on Nostr, nothing has confirmed it since,
|
|
* and nothing here will claim otherwise. A Cashu mint never carries it.
|
|
*/
|
|
export type MintStatus = 'online' | 'degraded' | 'offline' | 'unknown' | 'announced';
|
|
|
|
/**
|
|
* Which ecosystem a listing belongs to.
|
|
*
|
|
* Deliberately open rather than a closed union: `mints.type` is a stored TEXT column
|
|
* with no CHECK constraint, so an older build reading a database written by a newer one
|
|
* 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 & {});
|
|
|
|
/** One item of `GET /api/mints`. */
|
|
export interface MintListItem {
|
|
url: string;
|
|
host: string;
|
|
name: string | null;
|
|
icon: string | null;
|
|
/** 'cashu' or 'fedimint'. Rows written before the column existed read as 'cashu'. */
|
|
type: MintType;
|
|
status: MintStatus;
|
|
last_online: number | null;
|
|
review_count: number;
|
|
rating_avg: number | null;
|
|
score: number;
|
|
last_review_at: number | null;
|
|
version: string | null;
|
|
}
|
|
|
|
export interface ProbeSample {
|
|
ts: number;
|
|
ok: 0 | 1;
|
|
latency_ms: number | null;
|
|
}
|
|
|
|
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`.
|
|
*/
|
|
export interface MintDetail extends MintListItem, Partial<FedimintFields> {
|
|
description: string | null;
|
|
pubkey: string | null;
|
|
info: MintInfo | null;
|
|
nuts: string[];
|
|
first_seen: number;
|
|
last_probe: number | null;
|
|
updated_at: number;
|
|
rating_distribution: RatingDistribution;
|
|
/** Reviews in the last 90 days, for the verdict strip's activity cell. */
|
|
reviews_90d: number;
|
|
uptime_30d: number | null;
|
|
probes_recent: ProbeSample[];
|
|
}
|
|
|
|
/**
|
|
* What `ecosystem_json` holds for a Fedimint row, and what `GET /api/mints/:host`
|
|
* spreads across the detail payload for one.
|
|
*
|
|
* Flattened rather than nested so a Cashu payload is byte for byte what it always was:
|
|
* these keys are simply absent on one. A third ecosystem adds its own interface here
|
|
* and its own optional keys below; nothing existing has to move.
|
|
*/
|
|
export interface FedimintFields {
|
|
/** The `d` tag of the announcement: 64 hex characters. `host` is derived from it. */
|
|
federation_id: string;
|
|
/** Every `u` tag that was a usable invite code, in announcement order. */
|
|
invite_codes: string[];
|
|
/** The `modules` tag, split: `["ln", "mint", "wallet", "lnv2", "meta"]`. */
|
|
modules: string[];
|
|
/** The `n` tag, normalized — `bitcoin` and `mainnet` both arrive as `mainnet`. */
|
|
network: string | null;
|
|
/** `created_at` of the newest announcement seen for this federation. */
|
|
announced_at: number | null;
|
|
/** Who published that announcement. The `a` tag of a review points back at them. */
|
|
announcer_pubkey: string | null;
|
|
/**
|
|
* Who confirmed the status, when anything did: `"fedimint.observer"` today.
|
|
*
|
|
* null means nothing has, and then `status` is `announced` and never `online`. The
|
|
* page prints this next to the status, because "someone else says it is up" is a
|
|
* different claim from "we checked".
|
|
*/
|
|
status_source: string | null;
|
|
}
|
|
|
|
/** `GET /api/mints/:host` for a Fedimint federation: the detail plus its own fields. */
|
|
export type FedimintDetail = MintDetail & FedimintFields;
|
|
|
|
/**
|
|
* `GET /api/stats`.
|
|
*
|
|
* The four `mints_*` fields count Cashu mints and only Cashu mints, exactly as they did
|
|
* before federations were indexed: they are read by the pulse ticker, the /mints
|
|
* description and the home page, and a number that silently changed meaning would be
|
|
* worse than a new field. `cashu_total` is the same number under a name that says so,
|
|
* and every ecosystem added later gets its own `*_total` beside `fedimint_total`.
|
|
*/
|
|
export interface Stats {
|
|
mints_total: number;
|
|
mints_online: number;
|
|
mints_offline: number;
|
|
mints_degraded: number;
|
|
reviews_total: number;
|
|
last_review_at: number | null;
|
|
updated_at: number;
|
|
/** Same value as `mints_total`, named for the ecosystem it counts. */
|
|
cashu_total: number;
|
|
fedimint_total: number;
|
|
/** Federations a real check confirmed were up. Never inferred from an announcement. */
|
|
fedimint_online: number;
|
|
fedimint_offline: number;
|
|
/** Federations announced on Nostr that no check has ever confirmed. */
|
|
fedimint_announced: number;
|
|
/** Reviews of federations (`k` = 38173), included in `reviews_total`. */
|
|
fedimint_reviews: number;
|
|
}
|
|
|
|
/** `GET /api/health`. */
|
|
export interface Health {
|
|
status: 'ok' | 'degraded';
|
|
uptime_s: number;
|
|
last_probe_at: number | null;
|
|
last_discovery_at: number | null;
|
|
mints_tracked: number;
|
|
updated_at: number;
|
|
}
|
|
|
|
/**
|
|
* A mint's `/v1/info` response (NUT-06). Every field is optional: mints in the wild
|
|
* omit most of it, and the site must render whatever is there.
|
|
*/
|
|
export interface MintInfo {
|
|
name?: string;
|
|
pubkey?: string;
|
|
version?: string;
|
|
description?: string;
|
|
description_long?: string;
|
|
contact?: MintContact[];
|
|
motd?: string;
|
|
icon_url?: string;
|
|
urls?: string[];
|
|
time?: number;
|
|
tos_url?: string;
|
|
nuts?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface MintContact {
|
|
method?: string;
|
|
info?: string;
|
|
}
|
|
|
|
/** A review as rendered in the browser, parsed from a Nostr event. */
|
|
export interface Review {
|
|
id: string;
|
|
pubkey: string;
|
|
created_at: number;
|
|
mint_url: string;
|
|
/** null when the event carries no parseable rating. Never defaulted to 5. */
|
|
rating: number | null;
|
|
content: string;
|
|
}
|
|
|
|
/** Resolved kind-0 metadata for a reviewer. */
|
|
export interface Profile {
|
|
pubkey: string;
|
|
name?: string;
|
|
display_name?: string;
|
|
picture?: string;
|
|
nip05?: string;
|
|
/** false when the kind-0 lookup returned nothing, used for the provenance note. */
|
|
found: boolean;
|
|
}
|