@@ -0,0 +1,215 @@
|
||||
# NOTES.md: what the old codebase actually does
|
||||
|
||||
Findings from reading `cashu-mint-page/` (the previous cashumints.space frontend, React + Vite + NDK).
|
||||
Everything here is copied from the source, not from memory. File references are to the old repo.
|
||||
|
||||
## Nostr: event kinds
|
||||
|
||||
From `src/utils/ndk.ts`:
|
||||
|
||||
```ts
|
||||
export const MINT_RECOMMENDATION_KIND = 38000; // NIP-87: mint recommendation/review
|
||||
export const CASHU_MINT_KIND = 38172; // NIP-87: Cashu mint announcement
|
||||
```
|
||||
|
||||
- **38172**: mint announcement. Mint URL lives in the `u` tag. This is the only source of mint
|
||||
discovery in the old site: there is no hardcoded mint list anywhere in the repo
|
||||
(`src/hooks/useAllMints.ts` subscribes to `{kinds:[38172], limit:1000}` and builds the whole
|
||||
directory from what comes back).
|
||||
- **38000**: review / recommendation event.
|
||||
|
||||
## Nostr: relays
|
||||
|
||||
From `src/utils/ndk.ts`, `CASHU_RELAY_POOL` (this is the pool used for reads):
|
||||
|
||||
```
|
||||
wss://relay.damus.io
|
||||
wss://nos.lol
|
||||
wss://relay.azzamo.net
|
||||
wss://relay.cashumints.space
|
||||
```
|
||||
|
||||
`PROFILE_RELAYS` is the same list (with `nos.lol` and `relay.azzamo.net` duplicated, a bug that
|
||||
does nothing).
|
||||
|
||||
From `src/services/reviewPublisher.ts`, `RELAY_URLS` (the publish pool, a **different** list):
|
||||
|
||||
```
|
||||
wss://relay.cashumints.space
|
||||
wss://relay.damus.io
|
||||
wss://relay.snort.social
|
||||
wss://relay.primal.net
|
||||
```
|
||||
|
||||
Note the asymmetry: the old site publishes to `snort.social` and `primal.net` but never reads from
|
||||
them, so some of its own published reviews were invisible to it. The new backend reads the union of
|
||||
both lists (6 distinct relays) so nothing published by the old site is lost.
|
||||
|
||||
Also note `RELAY_URLS[0]` (`wss://relay.cashumints.space`) is used as the relay hint in the `a` tag.
|
||||
|
||||
## Nostr: review event wire format
|
||||
|
||||
Two code paths build events, and they disagree. What actually ships is `publishReview()`:
|
||||
|
||||
`src/services/reviewPublisher.ts` `publishReview()` (the one called by the live flow):
|
||||
|
||||
```ts
|
||||
event.kind = 38000;
|
||||
event.content = `[${rating}/5] ${content}`;
|
||||
event.tags = [
|
||||
['k', '38172'], // kind being recommended
|
||||
['u', mintInfo.url, 'cashu'], // mint URL, with 'cashu' as third element
|
||||
['d', mintInfo.pubkey] // mint's own pubkey from /v1/info (NIP-33 d tag)
|
||||
];
|
||||
```
|
||||
|
||||
`createReviewEvent()` in the same file builds a richer event that is **never published** (the flow
|
||||
calls `createReviewEvent` and then throws the result away, rebuilding the event inside
|
||||
`publishReview`). The dead version adds:
|
||||
|
||||
```ts
|
||||
['a', `38172:${mint.pubkey}:${mint.pubkey}`, 'wss://relay.cashumints.space'],
|
||||
['rating', String(rating)]
|
||||
```
|
||||
|
||||
**Consequence for reading:** most reviews in the wild have NO `rating` tag. The rating is encoded in
|
||||
the content as a `[N/5]` prefix. Any reader that only looks at the `rating` tag will see nothing.
|
||||
|
||||
### Rating parse order (from `src/utils/reviewHelpers.ts` `parseNIP87Review`)
|
||||
|
||||
1. `rating` tag, if present and 1..5.
|
||||
2. Else `^\s*\[([1-5])\/5\]` at the start of content.
|
||||
3. Else the loose regex `rating[:\s]*([1-5])|([1-5])[\/]5|([1-5])\s*star` (case insensitive).
|
||||
4. Else **default to 5**.
|
||||
|
||||
Step 4 is the reason every mint on the old site shows 5/5. Anything unparseable silently becomes a
|
||||
five star review. The new indexer stores `rating = NULL` instead and excludes NULLs from averages
|
||||
(the schema in BACKEND.md already allows this).
|
||||
|
||||
### Review filters used for reading (`src/hooks/useReviews.ts`)
|
||||
|
||||
Two filters, OR'd:
|
||||
|
||||
```ts
|
||||
// NIP-87 proper, only when the mint's /v1/info pubkey is known
|
||||
{ kinds:[38000], '#d':[mintPubkey], '#k':['38172'], limit }
|
||||
// legacy / URL based, always included
|
||||
{ kinds:[38000], '#u':[ mintUrl, noTrailingSlash, withTrailingSlash,
|
||||
noProtocol, httpsForm, httpForm ], limit }
|
||||
```
|
||||
|
||||
So a review is matched to a mint by `d`=mint pubkey **or** `u`=mint URL in any of 6 spellings.
|
||||
The new indexer resolves both the same way, but normalizes the `u` value instead of enumerating
|
||||
spellings.
|
||||
|
||||
### Dedupe
|
||||
|
||||
`aggregateReviews()` keys by **pubkey**, keeping the newest event per author (treating 38000 as a
|
||||
NIP-33 replaceable event, which it is: 30000-39999 is the addressable range). One review per npub
|
||||
per mint. The new backend keeps every event row (keyed by event id, per BACKEND.md schema) but
|
||||
applies the same newest-per-(pubkey, mint) rule when computing counts and averages.
|
||||
|
||||
## Mint URL normalization in the old code
|
||||
|
||||
There is no single normalizer. Three different ad-hoc ones:
|
||||
|
||||
1. `src/services/api.ts` `getMintInfoByDomain` / `getMintPubkey`:
|
||||
`url.replace(/^https?:\/\//,'').replace(/\/+$/,'')` then `https://${clean}/v1/info`.
|
||||
Forces https, strips protocol and trailing slashes, **keeps the path**. This one is fine.
|
||||
2. `src/hooks/useAllMints.ts` and `usePopularMints.ts`: `new URL(mintUrl).hostname`, then
|
||||
`.replace(/^mint\./,'').replace(/^www\./,'')` for the display name. The **hostname only**, so the
|
||||
path is dropped. `https://mint.minibits.cash/Bitcoin` and `https://mint.minibits.cash/Other`
|
||||
collapse to the same display name, and the map is keyed on the raw un-normalized `u` tag value,
|
||||
so `https://x.com` and `https://x.com/` are two separate mints in the directory.
|
||||
3. `src/utils/reviewHelpers.ts` `isReviewForThisMint`: lowercases, strips protocol and trailing
|
||||
slashes, then compares **domain only** (`split('/')[0]`), with a `www.` variant fallback.
|
||||
Explicit comment says subdomain and content matching were removed as too false-positive prone.
|
||||
|
||||
Net effect: mints are deduped by raw tag string (too strict, splits on trailing slash) while reviews
|
||||
are matched by bare domain (too loose, a review of `mint.minibits.cash/Bitcoin` counts for
|
||||
`mint.minibits.cash/Anything`). The new backend fixes both directions: one normalizer, path is part
|
||||
of identity, applied to mints and reviews alike (see `api/src/normalize.ts`).
|
||||
|
||||
## Old scoring
|
||||
|
||||
`src/hooks/usePopularMints.ts`:
|
||||
|
||||
```ts
|
||||
.filter(m => m.reviewCount >= 3)
|
||||
.sort((a,b) => b.averageRating !== a.averageRating
|
||||
? b.averageRating - a.averageRating
|
||||
: b.reviewCount - a.reviewCount)
|
||||
.slice(0, limit)
|
||||
```
|
||||
|
||||
Plain mean rating, descending, review count only as a tiebreak, with a hard `>= 3 reviews` floor.
|
||||
Combined with the "default to 5" parse fallback, this guarantees the leaderboard is a wall of 5.0s
|
||||
ordered arbitrarily. Replaced with the Bayesian weighted score from BACKEND.md.
|
||||
|
||||
**Worth keeping from the old scoring** (carried into the new implementation):
|
||||
|
||||
- **Dedupe by pubkey before averaging.** One npub, one vote per mint. Without this a single author
|
||||
can move a mint's average by replaying reviews. The Bayesian formula does not protect against
|
||||
this on its own, so the new backend keeps it.
|
||||
- **The `reviewCount >= 3` floor**, but repurposed. As a ranking rule it is too blunt (it hides
|
||||
new mints entirely). The prior weight `m = 5` in the Bayesian formula does the same job
|
||||
continuously, so the floor is dropped from ranking. It survives as a display rule only: the home
|
||||
page "top mints" grid shows the top 6 by score, and a mint with 1 review simply cannot out-score
|
||||
an established one, so no explicit floor is needed.
|
||||
- **The 30 day `since` window as a second filter alongside the unbounded one.** The old code fires
|
||||
`{kinds:[38000], limit:100}` and `{kinds:[38000], limit:100, since: now-30d}` together, which
|
||||
works around relays that return an arbitrary slice for an unbounded query. The discovery loop does
|
||||
the same thing (broad backfill plus a recent window) for the same reason.
|
||||
|
||||
Not kept: the `filter(m => m.reviewCount >= 3)` on the directory itself, the mean-only sort, and the
|
||||
default-to-5 rating.
|
||||
|
||||
## The rug bug, precisely
|
||||
|
||||
`src/pages/MintPage.tsx`:
|
||||
|
||||
```ts
|
||||
if (error) return <ErrorDisplay message={error} retryFn={fetchMintInfo} />; // line 105
|
||||
if (!mintInfo) return <ErrorDisplay message="No mint information available" .../>; // line 106
|
||||
```
|
||||
|
||||
`fetchMintInfo` calls `getMintInfoByDomain`, which is a direct browser `axios.get` to
|
||||
`https://{host}/v1/info` with a 10s timeout. If the mint is down, blocks CORS, or serves mixed
|
||||
content, `error` is set and the component returns before rendering the header, the reviews panel, or
|
||||
the review form. So a rugged mint, the one people most need to warn each other about, has no
|
||||
reachable review page. There is no cache: every page view re-fetches from the mint itself.
|
||||
|
||||
This is why the new architecture puts a server-side cache in front (`mints.info_json`, never
|
||||
overwritten on a failed probe) and renders the mint page from that cache, not from a live fetch.
|
||||
|
||||
## Other things found
|
||||
|
||||
- **No hardcoded mint list.** `public/sitemap.xml` has 5 static routes and no mint pages.
|
||||
`src/store/mintStore.ts` is an empty runtime cache. So the seed list in `api/src/seed.ts` had to
|
||||
be assembled from the mints the old site actually surfaces via Nostr, listed there with a comment.
|
||||
- **No wallet directory.** FRONTEND.md says to port wallet content from the old project, but the
|
||||
only thing there is an external link to `https://docs.cashu.space/wallets` in `Header.tsx:57` and
|
||||
`MobileMenu.tsx:65`. There is no wallet data to port. `/wallets` is built from a curated list
|
||||
instead, flagged in the summary.
|
||||
- **Routing was `/{host+path}`**, e.g. `/mint.minibits.cash/Bitcoin`, via a catch-all
|
||||
`<Route path="/*" element={<MintPage/>}/>`. The new site uses `/mint/{slug}` with the slug rules
|
||||
from BACKEND.md, and redirects are not attempted (old URLs were never indexed: no SEO, see
|
||||
PROJECT.md).
|
||||
- **`src/utils/nostr.ts` `hexToNpub` is fake.** It returns `` `npub${hex.slice(0,8)}...` ``, which is
|
||||
not bech32 and not a real npub, it just looks like one. The mockup's `npub7b2bfe92…` strings are
|
||||
this bug rendered. The new site encodes real npubs with `nip19.npubEncode` from nostr-tools.
|
||||
- **`src/utils/nutInfo.ts`** has human readable NUT names and descriptions, but several are wrong
|
||||
(NUT-04 is titled "Token Spending", NUT-05 "Token Melt", NUT-07 "Swap", NUT-08 "Melt"). The
|
||||
correct names are used instead, matching the mint page mockup's five highlighted rows.
|
||||
- Old site had `RelayPoolTest.tsx` and a lot of `console.log` debug output shipped to production,
|
||||
plus `loadDemoReviews()` returning fake Alice/Bob/Charlie reviews. None of that is carried over.
|
||||
|
||||
## Library choice
|
||||
|
||||
The old project depends on **both** `@nostr-dev-kit/ndk` and `nostr-tools`. NDK is used for all
|
||||
relay work; `nostr-tools` is a transitive dependency it never directly imports. NDK's browser bias
|
||||
(it assumes a persistent process and has no clean shutdown, see the `cleanupSharedNDK` comment
|
||||
"NDK doesn't have a disconnect method") makes it a poor fit for a Node indexer that must exit
|
||||
cleanly. The new code uses **nostr-tools** on both sides: `SimplePool` in the indexer, and the same
|
||||
library in the browser island, so there is one Nostr code path and one set of parse rules shared
|
||||
through `shared/`.
|
||||
Reference in New Issue
Block a user