Files
CashuMints.space/shared/src/types.ts
T
michilis 6f17b572b1 Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
2026-08-21 02:10:48 +02:00

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;
}