Skip to content

Buyer and seller each configure one mint list (agicash-like); fence hardcodes one mint and extra_mints is dead config #187

Description

@orveth

End goal

Buyer and seller each configure one mint list (agicash-like): the set of mints that side is willing to transact on. The buyer pays a seller from any of the buyer's mints — if the lists don't overlap, the buyer's wallet swaps across (mint quote at the seller's mint → melt from a buyer-funded mint → fresh ecash at the seller's mint), which is the cross-mint hop slice.

Today's config and fence can't express that. Two concrete defects, both in the same predicate, hence one issue and one review.

Current state — evidence

home::mint_allowed (crates/mobee-core/src/home.rs:750-758) is the single real-mint fence, enforced inside open_wallet_at_mint_async (buyer_fund.rs:85-106) and by resolve_realized_mint (authorize_pay.rs:471):

pub fn mint_allowed(mint_url: &str, allow_real_mints: bool) -> bool {
    if allow_real_mints {
        mint_url.strip_prefix("https://").is_some_and(|host| !host.is_empty())
    } else {
        mint_url == DEFAULT_MINT_URL
    }
}

1. Under the default posture the buyer has exactly one mint. With allow_real_mints == false, the fence admits only DEFAULT_MINT_URL, so no second mint's wallet can be opened at all. A buyer "mint list" of more than one entry is unrepresentable unless the real-money switch is on — i.e. multi-mint behaviour is structurally unreachable in the safe posture. The doc comment already reads "only the testnut/dev allow-list — today that is exactly DEFAULT_MINT_URL", so a list was the intent and the equality is the degenerate one-element case.

2. extra_mints is dead config. MobeeConfig::extra_mints is documented as the buyer wallet's additional allowed mints (home.rs:590-605) and wallet_ops.rs:167 composes "default mint_url first, then opt-in extra_mints (deduped)" — but mint_allowed never consults it. With the default allow_real_mints == false an extra mint is refused at wallet-open; with allow_real_mints == true the field is redundant because any https:// mint already passes. So the field cannot currently do the job its own docs describe.

Note also that the seller side already has its own list (accepted_mints, the accept policy) while the buyer side has extra_mints — two different shapes for what the end goal wants to be one symmetric concept per side.

Direction

Replace the equality with a real buyer mint list and keep the real-money switch orthogonal to it: the list says which mints this side transacts on; allow_real_mints says whether real-sats mints are permitted at all. Then extra_mints either becomes that list or is folded into it and removed, and the seller's accepted_mints is the mirror-image field.

Worth knowing while choosing defaults: test mints cannot pay each other's Lightning invoices, so a cross-mint hop between two fake mints is not exercisable end-to-end — the hop's live proof is a small real-sats trade. As of 2026-07-27 the only reachable public testnut probed was https://testnut.cashudevkit.org (cdk-mintd/0.17.0, NUT-04 + NUT-05 enabled for sat/bolt11); testnut.cashu.space and family are dead, which the repo already records (DEAD_TESTNUT_MINT_HOST, home.rs:67). So a multi-entry testnut list buys little on its own — the value is in expressing the real buyer/seller mint lists.

Teeth a fix should carry

  • The default posture admits every list entry and refuses everything else, including plausible near-misses (http:// scheme, empty host, a real-sats mint URL when the switch is off).
  • An operator-named list entry is honoured at wallet-open, not just at config parse — i.e. the defect in (2) cannot come back.
  • Buyer list and seller accepted_mints gate on the same predicate, so the two sides cannot drift (the property the current doc comment claims).
  • Turning on allow_real_mints remains the only way to reach a real-sats mint, independent of list membership.
  • Overlap vs no-overlap between the two lists is what selects direct payment vs the cross-mint hop; both branches asserted on the decision.

Related: the cross-mint hop slice (mint quote → melt → fresh ecash at the seller's mint) and #186 (hop fee-reserve reconciliation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions