# Cashu Mint Discovery — Sidebar Mint Lists from Contacts & Recommendations

## Overview

Add two mint discovery sections to the cashu.html sidebar that help users find mints:

1. **Mints from Contacts** — Fetches kind 10019 `CashuMintList` events from people the user follows, ranks mints by how many contacts use them
2. **Mint Recommendations** — Fetches kind 38000 `EcashMintRecommendation` events, shows community-recommended mints with their URLs

Each mint row has a **[+ Add]** button that adds the mint to the wallet (or to the setup textarea if no wallet exists yet).

## Data Sources

### Kind 10019 — CashuMintList

Published by every NIP-60 wallet user. Contains `["mint", "https://..."]` tags. This is a replaceable event — one per user. The `NDKCashuMintList` class has a `.mints` getter that extracts URLs.

**Query flow:**
1. Fetch kind 3 contact list for current user → extract `p` tags → list of followed pubkeys
2. Fetch kind 10019 events for those pubkeys → extract mint URLs
3. Count how many contacts use each mint → rank by popularity

### Kind 38000 — EcashMintRecommendation

Explicit mint recommendations. Contains `["u", "https://..."]` tags for mint URLs and `["k", "38002"]` tag to filter for Cashu mints specifically. The `NDKMintRecommendation` class has a `.urls` getter.

**Query flow:**
1. Fetch kind 38000 events with `#k` filter for `38002` (Cashu mint announcements)
2. Extract mint URLs from `u` tags
3. Group by URL, count recommendations

## Sidebar Layout

```
┌─────────────────────────────────┐
│  [SideNav Header]               │
├─────────────────────────────────┤
│                                 │
│  Mints from Contacts            │
│  ─────────────────────          │
│  Searching contacts...          │
│                                 │
│  (after loading:)               │
│  mint.minibits.cash        5 ⊕  │
│  mint.coinos.io            3 ⊕  │
│  stablenut.umint.cash      2 ⊕  │
│  21mint.me                 1 ⊕  │
│                                 │
│  ─────────────────────          │
│  Mint Recommendations           │
│  ─────────────────────          │
│  Searching...                   │
│                                 │
│  (after loading:)               │
│  mint.minibits.cash        4 ⊕  │
│  mint.lnbits.com           2 ⊕  │
│  testnut.cashu.space       1 ⊕  │
│                                 │
├─────────────────────────────────┤
│  リレー                          │
│  relay status...                │
├─────────────────────────────────┤
│  v0.2.78  [theme] [logout]      │
└─────────────────────────────────┘
```

The number next to each mint shows how many contacts use it (section 1) or how many recommendations it has (section 2). The ⊕ button adds the mint.

## Implementation Plan

### Step 1: Add `discoverMintsFromContacts` to cashu-wallet.mjs

New exported function in `cashu-wallet.mjs` that uses the controller's existing NDK instance:

```javascript
async function discoverMintsFromContacts(pubkey) {
  // 1. Fetch kind 3 contact list
  const contactEvent = await ndk.fetchEvent({
    kinds: [NDKKind.Contacts],  // kind 3
    authors: [pubkey]
  });

  if (!contactEvent) return [];

  // 2. Extract followed pubkeys from p tags
  const followedPubkeys = contactEvent.tags
    .filter(t => t[0] === 'p' && t[1])
    .map(t => t[1]);

  if (followedPubkeys.length === 0) return [];

  // 3. Fetch kind 10019 mint lists from followed users
  //    NDK fetchEvents supports arrays of authors
  //    Batch in chunks of ~100 to avoid filter size limits
  const mintCounts = new Map(); // mintUrl -> count

  const BATCH_SIZE = 100;
  for (let i = 0; i < followedPubkeys.length; i += BATCH_SIZE) {
    const batch = followedPubkeys.slice(i, i + BATCH_SIZE);
    const mintListEvents = await ndk.fetchEvents({
      kinds: [NDKKind.CashuMintList],  // 10019
      authors: batch
    });

    for (const event of mintListEvents) {
      const mintTags = event.tags.filter(t => t[0] === 'mint');
      for (const tag of mintTags) {
        const url = normalizeMintUrl(tag[1]);
        if (url) {
          mintCounts.set(url, (mintCounts.get(url) || 0) + 1);
        }
      }
    }
  }

  // 4. Return sorted by count descending
  return Array.from(mintCounts.entries())
    .map(([url, count]) => ({ url, count }))
    .sort((a, b) => b.count - a.count);
}
```

### Step 2: Add `discoverMintRecommendations` to cashu-wallet.mjs

```javascript
async function discoverMintRecommendations() {
  // Fetch kind 38000 events filtered for Cashu mint announcements
  const recEvents = await ndk.fetchEvents({
    kinds: [NDKKind.EcashMintRecommendation],  // 38000
    '#k': ['38002']  // filter for CashuMintAnnouncement kind
  });

  const mintCounts = new Map(); // mintUrl -> count

  for (const event of recEvents) {
    const urlTags = event.tags.filter(t => t[0] === 'u');
    for (const tag of urlTags) {
      const url = normalizeMintUrl(tag[1]);
      if (url) {
        mintCounts.set(url, (mintCounts.get(url) || 0) + 1);
      }
    }
  }

  return Array.from(mintCounts.entries())
    .map(([url, count]) => ({ url, count }))
    .sort((a, b) => b.count - a.count);
}
```

### Step 3: Expose both functions from the controller

Add both to the returned object from `createCashuWalletController`:

```javascript
return {
  // ...existing exports...
  discoverMintsFromContacts: () => discoverMintsFromContacts(pubkey),
  discoverMintRecommendations,
};
```

### Step 4: Add sidebar HTML structure

In `cashu.html`, add two container divs inside `#divSideNavBody`:

```html
<div id="divSideNavBody">
  <div id="divFiles"></div>
  <div id="divMintDiscovery">
    <div id="divContactMints">
      <div class="sidenavSectionTitle">Mints from Contacts</div>
      <div id="divContactMintsList">Searching contacts…</div>
    </div>
    <div id="divRecommendedMints">
      <div class="sidenavSectionTitle">Mint Recommendations</div>
      <div id="divRecommendedMintsList">Searching…</div>
    </div>
  </div>
</div>
```

### Step 5: Add sidebar CSS

Page-specific styles in `cashu.html` `<style>` block:

```css
#divMintDiscovery {
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.sidenavSectionTitle {
  font-size: 90%;
  font-weight: bold;
  color: var(--primary-color);
  border-bottom: 1px solid var(--muted-color);
  padding-bottom: 5px;
  margin-bottom: 5px;
}

.mintDiscoveryRow {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 5px;
  font-size: 75%;
  padding: 3px 0;
}

.mintDiscoveryUrl {
  flex: 1;
  word-break: break-all;
  color: var(--primary-color);
}

.mintDiscoveryCount {
  color: var(--muted-color);
  min-width: 20px;
  text-align: right;
}

.mintDiscoveryAdd {
  cursor: pointer;
  color: var(--accent-color);
  font-size: 110%;
  padding: 0 4px;
  border: 1px solid var(--muted-color);
  border-radius: 3px;
  background: none;
  line-height: 1;
}

.mintDiscoveryAdd:hover {
  background: var(--accent-color);
  color: var(--secondary-color);
}
```

### Step 6: Populate sidebar on openNav and on page load

In the `openNav()` function, replace the static text with the mint discovery sections. Trigger the async fetch:

```javascript
function openNav() {
  // ...existing hamburger logic...

  // Render mint discovery sections (populated async)
  renderMintDiscoverySidebar();

  // ...existing logout/theme hamburger logic...
}

async function renderMintDiscoverySidebar() {
  // Show loading state
  const divContactMintsList = document.getElementById('divContactMintsList');
  const divRecommendedMintsList = document.getElementById('divRecommendedMintsList');

  if (divContactMintsList) divContactMintsList.textContent = 'Searching contacts…';
  if (divRecommendedMintsList) divRecommendedMintsList.textContent = 'Searching…';

  // Fetch both in parallel
  try {
    const [contactMints, recommendedMints] = await Promise.all([
      walletController.discoverMintsFromContacts(),
      walletController.discoverMintRecommendations()
    ]);

    renderMintList(divContactMintsList, contactMints, 'No mints found from contacts');
    renderMintList(divRecommendedMintsList, recommendedMints, 'No recommendations found');
  } catch (error) {
    console.error('[cashu.html] Mint discovery failed:', error);
    if (divContactMintsList) divContactMintsList.textContent = 'Discovery failed';
    if (divRecommendedMintsList) divRecommendedMintsList.textContent = 'Discovery failed';
  }
}

function renderMintList(container, mints, emptyMessage) {
  if (!container) return;

  if (!mints || mints.length === 0) {
    container.textContent = emptyMessage;
    return;
  }

  container.innerHTML = mints.map(({ url, count }) => `
    <div class="mintDiscoveryRow">
      <div class="mintDiscoveryUrl">${url}</div>
      <div class="mintDiscoveryCount">${count}</div>
      <button class="mintDiscoveryAdd" data-mint-url="${url}" title="Add mint">⊕</button>
    </div>
  `).join('');

  // Bind add buttons
  container.querySelectorAll('.mintDiscoveryAdd').forEach(btn => {
    btn.addEventListener('click', () => handleAddDiscoveredMint(btn.dataset.mintUrl));
  });
}

async function handleAddDiscoveredMint(mintUrl) {
  if (walletController.hasWallet()) {
    // Add to existing wallet
    try {
      setStatus('Adding mint…');
      await walletController.addMint(mintUrl);
      await refreshAllWalletViews();
      setStatus('Mint added');
    } catch (error) {
      setStatus(error.message || 'Failed to add mint', true);
    }
  } else {
    // Append to setup textarea
    const current = taSetupMints.value.trim();
    const normalized = mintUrl.trim();
    if (!current.split('\n').map(l => l.trim()).includes(normalized)) {
      taSetupMints.value = current ? current + '\n' + normalized : normalized;
    }
    setStatus('Mint added to setup list');
  }
}
```

### Step 7: Also trigger discovery on page load (background)

Start the mint discovery fetch after wallet initialization completes, so the data is ready when the user opens the sidebar:

```javascript
// After wallet init in main()
// Pre-fetch mint discovery data in background
walletController.discoverMintsFromContacts().catch(() => {});
walletController.discoverMintRecommendations().catch(() => {});
```

Optionally cache the results in the controller so repeated sidebar opens don't re-fetch.

## Files to Modify

| File | Changes |
|---|---|
| `www/js/cashu-wallet.mjs` | Add `discoverMintsFromContacts()` and `discoverMintRecommendations()` functions, expose from controller |
| `www/cashu.html` — HTML | Add mint discovery container divs inside `#divSideNavBody` |
| `www/cashu.html` — CSS | Add styles for `.sidenavSectionTitle`, `.mintDiscoveryRow`, `.mintDiscoveryUrl`, `.mintDiscoveryCount`, `.mintDiscoveryAdd` |
| `www/cashu.html` — JS | Add `renderMintDiscoverySidebar()`, `renderMintList()`, `handleAddDiscoveredMint()`, update `openNav()` |

## Technical Notes

- The cashu-wallet controller already creates its own NDK instance at [line 52](www/js/cashu-wallet.mjs:52) connected to the user's relays, so it can fetch kind 3 and kind 10019 events directly without going through the worker.
- Kind 3 is the user's contact list — `["p", "<hex-pubkey>"]` tags list followed users.
- Kind 10019 is `CashuMintList` — `["mint", "https://..."]` tags list mint URLs. One per user (replaceable).
- Kind 38000 is `EcashMintRecommendation` — `["u", "https://..."]` tags for mint URLs, `["k", "38002"]` to filter for Cashu.
- Batching the kind 10019 fetch in chunks of 100 pubkeys avoids relay filter size limits.
- The ⊕ button behavior is context-aware: adds to wallet if one exists, or appends to setup textarea if in setup mode.
