Ship LNURL mint pages, indexing UI, and reviews rewrite.

Add lnurl list/detail routes, OG fixtures, i18n strings, and the write/
index client flows so the site surfaces the new mint type end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:43 +02:00
co-authored by Cursor
parent 2a9444942b
commit c74c7fc187
66 changed files with 7603 additions and 1241 deletions
+59 -9
View File
@@ -16,8 +16,9 @@
* there is nothing to translate in a lookup anyway.
*/
import {
federationIdFromKey, federationIdFromSlug, mintUrlsFromEvent, normalizeMintUrl,
reviewEcosystem, reviewTargetId, type MintListItem, type NostrEventLike,
baseUrlFromKey, federationIdFromKey, federationIdFromSlug, hostIdentifier, mintUrlsFromEvent,
normalizeMintUrl, reviewEcosystem, reviewTargetId,
type MintListItem, type NostrEventLike,
} from '@cashumints/shared';
import { displayDomain } from './format';
@@ -30,13 +31,14 @@ export interface FeedTarget {
name: string;
/** The label a card prints: a domain, or a shortened federation id. */
domain: string;
/** `cashu` or `fedimint`. Picks both the resolver and the link's route. */
/** `cashu`, `fedimint` or `lnurl`. Picks both the resolver and the link's route. */
type: string;
/**
* The `d` identifier reviews of this thing carry, when that is how it is found.
*
* A federation id. null for a mint, which is resolved through its `u` tag instead —
* that is how every legacy review points at one, and changing it would strand them.
* A federation id, or an LNURL mint's normalized host. null for a Cashu mint, which is
* resolved through its `u` tag instead — that is how every legacy review points at
* one, and changing it would strand them.
*/
ref: string | null;
}
@@ -74,6 +76,29 @@ export function resolveFeedTarget(event: NostrEventLike, index: FeedIndex): Feed
return d ? index.byRef.get(d.toLowerCase()) ?? null : null;
}
/*
* LNURL resolves by `d` and then by `u`, because both are genuinely used.
*
* The `d` a review carries is either the mint's funding-node pubkey or its host, and
* the feed index only knows the host — the list payload carries no pubkey. So a
* pubkey-`d` review falls through to `u`, which an LNURL review can actually be
* resolved by: unlike a federation's opaque invite code, it is a real URL that the
* same normalizer the Cashu branch uses can turn into a row key.
*/
if (ecosystem === 'lnurl') {
const d = reviewTargetId(event);
const byRef = d ? index.byRef.get(d.trim().toLowerCase()) : undefined;
if (byRef) return byRef;
for (const raw of mintUrlsFromEvent(event)) {
const normalized = normalizeMintUrl(raw);
if (!normalized) continue;
const found = index.byUrl.get(`lnurl:${normalized.url}`);
if (found) return found;
}
return null;
}
if (ecosystem !== 'cashu') return null;
const raw = mintUrlsFromEvent(event)[0];
@@ -83,9 +108,21 @@ export function resolveFeedTarget(event: NostrEventLike, index: FeedIndex): Feed
return index.byUrl.get(normalized.url) ?? null;
}
/** The page a card links to. One route per ecosystem, decided in one place. */
/**
* The page a card links to. One route per ecosystem, decided in one place.
*
* The default is `/mint/`, not the newest ecosystem: a row of some type this build has
* never heard of is far more likely to be an older one, and every row written before
* `mints.type` existed means Cashu.
*/
const ROUTES: Record<string, string> = {
fedimint: '/fedimint',
lnurl: '/lnurl-mint',
cashu: '/mint',
};
export function targetPath(target: Pick<FeedTarget, 'type' | 'host'>): string {
return `/${target.type === 'fedimint' ? 'fedimint' : 'mint'}/${target.host}`;
return `${ROUTES[target.type] ?? '/mint'}/${target.host}`;
}
/**
@@ -103,14 +140,27 @@ export function targetPath(target: Pick<FeedTarget, 'type' | 'host'>): string {
export function feedTargets(...lists: Array<readonly MintListItem[]>): FeedTarget[] {
return lists.flat().map((item) => {
const federationId = federationIdFromKey(item.url);
const domain = federationId ? federationIdFromSlug(item.host) : displayDomain(item.url);
const lnurlBase = baseUrlFromKey(item.url);
/*
* An LNURL row's key is `lnurl:https://host`, so the label has to come out of the
* key rather than off `item.url` directly — `displayDomain` on the raw key would
* print the scheme prefix at a reader. Its `ref` is the host identifier, which is
* the `d` a review carries whenever the mint has no funding-node pubkey; reviews
* that carry the pubkey instead resolve through `u`, which is why this does not
* need one.
*/
const domain = federationId
? federationIdFromSlug(item.host)
: displayDomain(lnurlBase ?? item.url);
return {
url: item.url,
host: item.host,
name: item.name ?? domain.split('/')[0] ?? domain,
domain,
type: item.type,
ref: federationId,
ref: federationId ?? (lnurlBase ? hostIdentifier(lnurlBase) : null),
};
});
}