Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
This commit is contained in:
+133
-1
@@ -8,6 +8,12 @@
|
||||
* `/v1/info` (NUT-04 and NUT-05), plus `status` and `last_online`. The cache is the
|
||||
* point: an offline mint still has its last known configuration, and the copy says
|
||||
* "had" rather than "has" so nobody reads a stale flag as a live one.
|
||||
*
|
||||
* Two branches, chosen by `type` and never mixed. Cashu gets the six states below,
|
||||
* every one of them read out of a `/v1/info` this site fetched itself. Fedimint gets
|
||||
* two, because a federation publishes no capability list to read: a real check said the
|
||||
* guardians are down, or nothing has ever confirmed the federation at all. There is no
|
||||
* Fedimint equivalent of "melt only" and this file does not invent one.
|
||||
*/
|
||||
import type { MintInfo, MintStatus } from './types.js';
|
||||
import { nutEntry } from './nuts.js';
|
||||
@@ -73,12 +79,16 @@ function isLightningMethod(method: unknown): boolean {
|
||||
export type MintWarningSeverity = 'critical' | 'warning';
|
||||
|
||||
export type MintWarningKind =
|
||||
// Cashu
|
||||
| 'gone' // offline 30 days or more, or never reached at all
|
||||
| 'melt-disabled' // sats can get in but not out
|
||||
| 'frozen' // neither in nor out
|
||||
| 'offline-long' // offline 7 to 30 days
|
||||
| 'melt-only' // minting off, melting still works
|
||||
| 'offline'; // offline under 7 days
|
||||
| 'offline' // offline under 7 days
|
||||
// Fedimint
|
||||
| 'fedimint-offline' // a real check reported the guardians down
|
||||
| 'never-confirmed'; // announced on Nostr, and nothing has ever confirmed it
|
||||
|
||||
export interface MintWarning {
|
||||
kind: MintWarningKind;
|
||||
@@ -108,8 +118,18 @@ export interface MintWarning {
|
||||
export type WarningStrings = (key: string, vars?: Record<string, string | number>) => string;
|
||||
|
||||
export interface MintWarningInput {
|
||||
/**
|
||||
* Which ecosystem this is. Absent reads as `cashu`, so every existing caller keeps
|
||||
* exactly the behaviour it had; a `fedimint` row takes the separate branch below,
|
||||
* which shares the severity machinery and none of the NUT reasoning.
|
||||
*/
|
||||
type?: string;
|
||||
status: MintStatus | string;
|
||||
last_online: number | null;
|
||||
/** Fedimint: `created_at` of the newest announcement, for "announced {date}". */
|
||||
announced_at?: number | null;
|
||||
/** Fedimint: used only to decide whether reviews are the page's one sign of life. */
|
||||
last_review_at?: number | null;
|
||||
/** Cached `/v1/info`, as `GET /api/mints/:host` returns it. */
|
||||
info?: MintInfo | null;
|
||||
/**
|
||||
@@ -183,6 +203,21 @@ export const WARNING_COPY_EN: Record<string, string> = {
|
||||
'offline.body': 'Details were last confirmed then. You can still write a review.',
|
||||
'offline.meta': 'Offline since {date}',
|
||||
|
||||
'fedimintOffline.lead': 'Reported offline.',
|
||||
'fedimintOffline.body.since':
|
||||
'The last check that reached this federation was on {date} ({days} ago). Its guardians have not answered since. Treat funds held there as at risk, and do not join it with new funds.',
|
||||
'fedimintOffline.body.never':
|
||||
'No check of this federation has ever reached it, and the most recent one failed. Treat funds held there as at risk, and do not join it with new funds.',
|
||||
'fedimintOffline.meta.since': 'Reported offline since {date}',
|
||||
'fedimintOffline.meta.never': 'Reported offline, never once reached',
|
||||
|
||||
'neverConfirmed.lead': 'Announced, never confirmed.',
|
||||
'neverConfirmed.body.reviewed':
|
||||
'This federation was announced on Nostr on {date}, {days} ago, and nothing has confirmed since then that it is running. The reviews below are the only sign of life on this page; everything else came from the announcement.',
|
||||
'neverConfirmed.body.quiet':
|
||||
'This federation was announced on Nostr on {date}, {days} ago, nothing has confirmed since then that it is running, and nobody has reviewed it recently. Everything below came from the announcement.',
|
||||
'neverConfirmed.meta': 'Announced {date}, never confirmed',
|
||||
|
||||
'also.meltDisabled': 'Before going offline it had also disabled withdrawals.',
|
||||
'also.mintDisabled': 'Before going offline it had also disabled new minting.',
|
||||
'also.offline': 'It has also been offline since {date} ({days}).',
|
||||
@@ -213,8 +248,23 @@ const englishStrings: WarningStrings = (key, vars) => {
|
||||
/** Severity order, highest first. Index 0 of the returned list is the one to show. */
|
||||
const ORDER: MintWarningKind[] = [
|
||||
'gone', 'melt-disabled', 'frozen', 'offline-long', 'melt-only', 'offline',
|
||||
// Appended rather than interleaved. The Fedimint kinds never share a list with the
|
||||
// Cashu ones (the two branches are exclusive), so their position relative to those
|
||||
// is arbitrary — and appending leaves every existing rank exactly where it was.
|
||||
'fedimint-offline', 'never-confirmed',
|
||||
];
|
||||
|
||||
/**
|
||||
* How stale an unconfirmed announcement has to be before it is worth saying so.
|
||||
*
|
||||
* A federation announced last week that nothing has checked is not news: nothing has
|
||||
* had the chance. A month of silence is.
|
||||
*/
|
||||
export const NEVER_CONFIRMED_AFTER_S = 30 * 24 * 60 * 60;
|
||||
|
||||
/** Reviews inside this window count as the page having a pulse. Matches `reviews_90d`. */
|
||||
const RECENT_REVIEW_S = 90 * 24 * 60 * 60;
|
||||
|
||||
const defaultDate = (unix: number): string =>
|
||||
new Date(unix * 1000).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
||||
@@ -232,6 +282,88 @@ const defaultMonth = (unix: number): string =>
|
||||
export function getMintWarnings(
|
||||
mint: MintWarningInput,
|
||||
options: MintWarningOptions = {},
|
||||
): MintWarning[] {
|
||||
if (mint.type === 'fedimint') return fedimintWarnings(mint, options);
|
||||
return cashuWarnings(mint, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* What can be said about a federation, which is much less than about a mint.
|
||||
*
|
||||
* A federation publishes no capability list, so there is no NUT-04/NUT-05 equivalent to
|
||||
* read and nothing here invents one: the melt-only, withdrawals-disabled and frozen
|
||||
* banners have no Fedimint counterpart and never will until federations publish
|
||||
* something a check can actually read. That leaves two honest things to say.
|
||||
*
|
||||
* The first is that a check reported the guardians down — a real result, from
|
||||
* `status_source`, never inferred from the absence of an announcement. The second is
|
||||
* the soft one: this federation has been sitting on Nostr for a month or more and
|
||||
* nothing has ever confirmed it is running, so everything on its page came from a
|
||||
* stranger's announcement rather than from the federation itself.
|
||||
*
|
||||
* Note what is deliberately missing: a federation that no check reached is `announced`,
|
||||
* not `offline`, and gets the soft banner rather than the "likely gone" one. Not
|
||||
* knowing is not the same as knowing it is dead.
|
||||
*/
|
||||
function fedimintWarnings(
|
||||
mint: MintWarningInput,
|
||||
options: MintWarningOptions = {},
|
||||
): MintWarning[] {
|
||||
const now = options.now ?? Math.floor(Date.now() / 1000);
|
||||
const date = options.formatDate ?? defaultDate;
|
||||
const s = options.strings ?? englishStrings;
|
||||
|
||||
const days = (since: number): string =>
|
||||
s('dayCount', { n: Math.max(0, Math.floor((now - since) / 86400)) });
|
||||
|
||||
if (mint.status === 'offline') {
|
||||
const since = mint.last_online;
|
||||
return [
|
||||
{
|
||||
kind: 'fedimint-offline',
|
||||
severity: 'critical',
|
||||
lead: s('fedimintOffline.lead'),
|
||||
body:
|
||||
since !== null
|
||||
? s('fedimintOffline.body.since', { date: date(since), days: days(since) })
|
||||
: s('fedimintOffline.body.never'),
|
||||
meta:
|
||||
since !== null
|
||||
? s('fedimintOffline.meta.since', { date: date(since) })
|
||||
: s('fedimintOffline.meta.never'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Anything a check has confirmed up needs no banner, and a fresh announcement no
|
||||
// check has got to yet has not earned one either.
|
||||
if (mint.status !== 'announced') return [];
|
||||
|
||||
const announced = mint.announced_at ?? mint.first_seen ?? null;
|
||||
if (announced === null || now - announced < NEVER_CONFIRMED_AFTER_S) return [];
|
||||
|
||||
const reviewed =
|
||||
mint.last_review_at !== null &&
|
||||
mint.last_review_at !== undefined &&
|
||||
now - mint.last_review_at <= RECENT_REVIEW_S;
|
||||
|
||||
return [
|
||||
{
|
||||
kind: 'never-confirmed',
|
||||
severity: 'warning',
|
||||
lead: s('neverConfirmed.lead'),
|
||||
body: s(`neverConfirmed.body.${reviewed ? 'reviewed' : 'quiet'}`, {
|
||||
date: date(announced),
|
||||
days: days(announced),
|
||||
}),
|
||||
meta: s('neverConfirmed.meta', { date: date(announced) }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function cashuWarnings(
|
||||
mint: MintWarningInput,
|
||||
options: MintWarningOptions = {},
|
||||
): MintWarning[] {
|
||||
const now = options.now ?? Math.floor(Date.now() / 1000);
|
||||
const date = options.formatDate ?? defaultDate;
|
||||
|
||||
Reference in New Issue
Block a user