Skip to content

one mint concept: retire the real-mint fence and the play-money class, leaving the seat's accepted list as the only gate - #964

Open
maxy-player wants to merge 6 commits into
MakePrisms:mainfrom
maxy-player:feat/one-mint-concept
Open

one mint concept: retire the real-mint fence and the play-money class, leaving the seat's accepted list as the only gate#964
maxy-player wants to merge 6 commits into
MakePrisms:mainfrom
maxy-player:feat/one-mint-concept

Conversation

@maxy-player

@maxy-player maxy-player commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Overview

A mint is the service that issues and redeems the digital-cash tokens a maxplayer seat pays
and gets paid with. Until now the code carried a second idea on top of that: some mints were
"real" and some were "test", a allow_real_mints switch fenced the real ones off, a "play-money"
class existed for the test ones, and a test mint (testnut) was the shipped default.

The owner's ruling (03:40Z, the night of 2 Sep 2026): a mint is just a mint. There is no "real mint" or
"testmint" concept; the seller chooses what mint to accept or not. The shipped default is minibits.

This PR makes the code say exactly that. The seat's configured accepted list is the whole
admission decision.
If a mint URL is on the operator's list, the seat deals with it; if it is
not, the seat does not. The real-mint fence, its config key, its environment variable, the
play-money class and the test-mint default all go with the retired concept. The one shape rule that
remains — "is this string even a mint URL" — is now implemented correctly.

What changed

  • The fence is gone. allow_real_mints is no longer a setting and MAXPLAYER_ALLOW_REAL_MINTS
    no longer does anything. An old config.toml that still carries the key loads with a one-line
    warning instead of refusing to boot; the environment variable is reserved so an old export
    cannot refuse a boot either. Those five lines of back-compat are the only places the old names
    survive.
  • The play-money class is deleted (MoneyType::Play, refuse_silent_play_money) — commit
    0e80e32, the second of six. It goes with the retired concept on the owner's ruling of
    3 Sep 2026 23:54Z; it is not held open for a revert.
  • The test-mint default is gone. DEFAULT_MINT_URL no longer exists anywhere outside untouched
    web fixtures; the dead testnut.cashu.space host survives only as a migration source that
    rewrites an old config to minibits. Test fixtures use one clearly named constant of their own.
  • Prose retired with the concept: the served skill docs, README, quickstart, Docker doc, the
    smoke script and code comments no longer teach "real mint" versus "test mint".
  • The mint-URL shape check is now correct. mint_url_supported (home.rs:1473-1521) accepts
    http:// or https:// with a real, non-empty host, and nothing else. It refuses what the old
    authority tail-check waved through — a non-numeric port (https://mint.example:abc) and an
    unterminated IPv6 literal (https://[::1) — and admits a sidecar on IPv6 loopback
    (http://[::1]:3338, https://[::1]).

Six append-only commits over upstream/main 8c3bc9b, 22 files, +659/−1012. Zero manifest
or dependency changes
(Cargo.toml/Cargo.lock touched: 0). credential_proxy.rs is not among
the 22 files.

Grade versus release. The forge advisor graded this head A–L passing at 14e149b. That is a
grade only; it authorises no push, PR, merge or payment by itself. The push and this PR were
released by maxie and confirmed by the owner. The merge is the owner's act.

The URL validator, and why it is written by hand

home.rs:1473-1521 implements six steps: scheme must be https:// or http://; the authority is
the text up to the first /, ? or #; the host is what follows the last @; a bracketed IPv6
literal must close and may be followed only by : and an all-digit port; an unbracketed host
carries no brackets and at most one :, followed by an all-digit port; and the host must be
non-empty with no whitespace.

The test (home::tests::mint_url_supported_requires_a_scheme_and_a_real_host) names 9 admitted
cases (home.rs:2681-2690) and 19 refused (14 malformed authorities at :2697-2711, 5 wrong or
missing schemes at :2718).

url::Url::parse is deliberately not used. It is a browser-compatibility parser: Url::parse( "http:///x") returns Ok with host "x" (WHATWG special schemes skip repeated slashes), so a
rule of "scheme ∈ {http, https} and non-empty host" built on it would admit http:///x, which
this whitelist refuses. Independently, url 2.5.8 is a dev-dependency of maxplayer-core
(Cargo.toml:138, under [dev-dependencies] at :113; [dependencies] opens at :57), and the
only use url:: in the crate sit inside #[cfg(test)] modules. Reaching it from product code
would have changed a money-adjacent crate's shipped dependency set, which is not this job.

http:// on loopback stays admitted on purpose — a seat's own sidecar mint runs there
(home.rs:2681-2682 and :2689-2690, reason at :1465-1467).

A test that was silently not running — disclosed

At the earlier head f719f1e, two #[test] attributes were stacked on the new URL test and
shipped_defaults_are_real_money_and_usable had none, so that test compiled and ran zero
times. The compiler had said so (duplicate_macro_attributes, "function … is never used") and it
was read past. Repaired at 5e7c68e: exactly one attribute on each.

The inventory, measured on cargo test … --lib -- --list at both heads: executed test lines
1365 at BOTH heads
(the duplicate attribute produced two entries under one name; the harness's
own running 1365 tests = 1361 passed + 1 failed + 3 ignored), and unique test names 1364 at
f719f1e → 1365 at 14e149b
— exactly one name added, home::tests::shipped_defaults_are_real_money_and_usable, nothing removed. The unchanged total is why a count is not coverage.

Test evidence, with its bounds

This is not CI-green. The evidence is two independent local ladders on the five cargo test
steps of ci.yml, plus the relay-policy step. Ordinals below are per ladder.

Worker ladder (six steps, --locked, at 14e149b): step 1 -p maxplayer-core 381/0 · step 2
--features acp 430/0 (1 ignored) · step 3 --release --features acp,gateway,git-delivery,wallet
1361 passed / 1 failed / 3 ignored, exit 101 · step 4 -p maxplayer 136/0 · step 5
--features acp,wallet 162/0 (1 ignored) · step 6 -p maxplayer-relay-write-policy 12/0.

hearth ladder (five steps, independent detached worktree, at 14e149b): hearth steps 1, 2, 4
and 5 exit 0; hearth step 3 exits 101 on
credential_proxy::tests::a_declared_over_cap_body_is_refused_before_the_upstream_sees_it
(credential_proxy.rs:3520:14, a ConnectionReset). hearth measured that test at 1 pass / 5
failures over 6 attempts at BOTH base 8c3bc9b and head 14e149b
. It is a pre-existing
flake, not a regression
: same rate at base, and credential_proxy.rs is not in this diff.

The live-mints money-path job (ci.yml:233) was not run by anyone here — it reaches a live
third-party mint, which the standing money bound forbids.

Who measured what. Worker: its own six-step ladder and the job report. hearth: the independent
five-step ladder and the graded package. Advisor: graded A–L from the diff and ran no code. maxie:
re-derived the lock, the scope, the validator span, the 19/9 case gate and the H name-set diff.

Forensic detail — commits, gates, locators (re-derived at 14e149b)

Commits over 8c3bc9b, append-only, no amend/rebase/force:

bd182e1 one mint: the seat's configured list is the only gate on a mint
0e80e32 one mint: delete the play-money class and the legacy dev-mint constant
4673cfa one mint: parse the mint URL properly, and finish retiring the concept in prose
f719f1e one mint: drop the dead real-mint-fence disjunct from the MCP prereq hint
5e7c68e one mint: register the shipped-defaults test that lost its attribute
14e149b one mint: make the mint-URL host validator correct, by hand

git diff --shortstat 8c3bc9b 14e149b: 22 files changed, 659 insertions(+), 1012 deletions(-).

Whole-tree gates at 14e149b (git grep, hits / files):
-w -E 'allow_real_mints|RealMintDisallowed' 4 / 2 — the back-compat drop at
home.rs:1719/1723/1725 and a test asserting the name is absent at wallet_ops.rs:1121 ·
MAXPLAYER_ALLOW_REAL_MINTS 1 / 1 — the reserved env var at home.rs:1840 ·
-w DEFAULT_MINT_URL outside web/network/test 0 ·
MoneyType::Play|refuse_silent_play_money 0 · LEGACY_TESTNUT_MINT_URL 0 ·
real-mint fence 0 · const FIXTURE_MINT_URL declarations 8 ·
-w -i testnut 118 / 27, every hit marked (cfg-test 71 · fixture 31 · smoke-dry-run 11 ·
infra 4 · migration 1; unmarked 0).

DEAD_TESTNUT_MINT_HOST ("testnut.cashu.space", migration source only): home.rs:75
declaration · :1620 doc · :1677 migration guard · :2575 migration test.

Validator case verdicts at 14e149b, printed from the code: 7 original admitted → 7 true;
12 original malformed + 5 scheme cases → 17 false; https://mint.example:abc and https://[::1
false (both were true at f719f1e); http://[::1]:3338 and https://[::1]true. http:///x refused: its authority is empty under step 2.

Sequencing. Same base as #962
(8c3bc9b); independent trees. Owner's merge order: #962 → this PR → slice 2 of the ecash lane,
which is held local, unpushed, and rebases onto this head before its own review.

w-ecash-mutual-credit added 6 commits September 3, 2026 08:14
Owner ruling (Bob, 2 Sep 2026): "the concept of real mint doesn't make
sense - its just a mint. the seller chooses what mint to accept or not"
and "there should be no 'testmint' concept". One rule is left: a mint is
usable by this seat when it is on this seat's configured list, and not
otherwise.

- `allow_real_mints` is gone: the config field, its serde default, the env
  row, and the config-template block. A config.toml still carrying the key
  LOADS — it is dropped before the `deny_unknown_fields` parse and the drop
  is announced once, so no operator's boot breaks. `MAXPLAYER_ALLOW_REAL_MINTS`
  joins the reserved env seams for the same reason.
- `home::mint_allowed(url, switch)` becomes `home::mint_url_supported(url)`:
  a SHAPE rule (http:// or https:// with a host), not a policy. `http://` is
  admitted deliberately — a seat's own sidecar mint runs on loopback.
- `WalletOpsError::RealMintDisallowed` is gone with the duplicate fences that
  raised it: send/receive/melt already gate on the configured list, and the
  seller's redeem path already gates on the stored creq's accepted set.
  `plan_payment` / `select_source_mint` / `AwardFilters` lose the parameter,
  so their callers change with them.
- `DEFAULT_MINT_URL` loses its name. Nothing in the product is called the
  default mint except the configured list (shipped default: minibits), which
  is now what `default_mint()`'s fallback, the seller setup seed and the
  dead-host migration read. Test fixtures use `FIXTURE_MINT_URL`, a plain
  example host.
- Product docs that taught the removed concept are rewritten to the one rule:
  the crossmint smoke script (its dead env export was already a no-op), both
  served skill.md files, README's play-money paragraph, docs/DOCKER.md and
  docs/BUYER-QUICKSTART.md.

The money class (`MoneyType`, `is_autopay_mint`, `refuse_silent_play_money`
and the play-money row marker) is still PRESENT here, re-anchored on
`LEGACY_TESTNUT_MINT_URL` so this commit compiles and the split is reviewable
one change at a time. The next commit deletes it.
## Removed by the owner's one-mint ruling
Bob, thread messages 1544914875195723838 (2026-09-02 03:40:09Z) and
1544916134657269771 (03:45:09Z): a mint is just a mint, the seller chooses
what mint to accept, and there is no "testmint" concept.

This removes the MakePrisms#445 `refuse_silent_play_money` refusal and the
`MoneyType` class, both dead once the default host is gone — the class
could only ever answer `Play` for one hardcoded URL, and the refusal could
only ever fire for a home whose default was that URL, telling the operator
to affirm play money at a host nothing seeds any more.

Gone with them: `is_autopay_mint`, the auto-complete branch of
`mint_async` (every mint now returns the invoice for the caller to pay),
the `play_money=true` row marker on `wallet balance` / `wallet mints list`
/ the setup summaries, the seven tests that covered them, and
`pub const LEGACY_TESTNUT_MINT_URL` — the host it named is not dead and
not special, so it gets no constant.

`DEAD_TESTNUT_MINT_HOST` and its bootstrap migration are untouched: that
host really is dead and the migration only ever removes it.

Two consequential edits, named so the scope is legible: the two balance
tests that only assert on a stray mint's row and the totals line needed
*a* configured default rather than *the* auto-paying one, so each has one
token changed, `seed_testnut_default_home` -> `seed_fixture_default_home`;
and a stale doc line plus a duplicate `#[cfg(feature = "wallet")]` left
above that fixture helper by an earlier edit go here too.
…t in prose

Advisor DENY of 0e80e32 on checks D and F; appended, not rewritten.

D — `mint_url_supported` called "host" everything after the scheme and
only checked that tail was non-empty, so `http:///x`, `http://:8080` and
`http:// ` were all waved through. It now splits the authority at the
first `/`, `?` or `#`, drops any `user@` prefix and any numeric `:port`,
and requires a non-empty host with no whitespace. `http://` STAYS
ADMITTED — a seat's own sidecar mint runs on loopback, and the doc
comment saying so is unchanged. New test
`mint_url_supported_requires_a_scheme_and_a_real_host` names all twelve
malformed cases plus the admitted forms and the non-http schemes.

F — fourteen survivor lines still taught the retired real-mint /
play-money concept, in prose the G1-G4 census never searched for:
- `web/app/.well-known/skills/debug-buying/skill.md` — SERVED to
  operators, said the hop happens "if real mints are allowed" and that a
  mismatch means being pinned to "test-only mints". Both are gone; the
  paragraph now says a mismatch blocks only when there is no route.
- doc comments in `authorize_pay.rs` (x3), `buyer_fund.rs`,
  `buyer/lifecycle.rs`, `job_lifecycle.rs` (x2) that named a fence which
  no longer exists, and one in `wallet_cli.rs` naming the "held
  play-money class" that the previous commit deleted.
- `scripts/crossmint-smoke.sh`: `arm_real_mints` is renamed
  `arm_mint_lists`, for what it actually does — export the accepted and
  extra mint lists and the budget cap. All three call sites updated.

The parked testnut dry-run lines in the smoke script are untouched.
…hint

Advisor's ninth F line (maxie's ruling, 2026-09-03 17:08Z), appended.

`with_prereq_hint` matched `real-mint fence` to decide that a tool failure
was a funding prerequisite. Both producers of that string —
crossmint.rs's two fence refusals — went with the fence itself, so at this
head the phrase had exactly one hit tree-wide: this disjunct. It was an
unreachable branch asserting in product code that the retired class still
gates a payment.

Deleted; the `;` moves onto the `mint_unreachable` disjunct. The other
three disjuncts are untouched. No test referenced the phrase — the
funds-prereq test drives the "no balance at any accepted mint" disjunct —
and the tree-wide count for `real-mint fence` is now 0.
Advisor check H. `home.rs` carried two `#[test]` attributes stacked on
`mint_url_supported_requires_a_scheme_and_a_real_host` and none on
`shipped_defaults_are_real_money_and_usable`, so that test was compiled
but never run. The defect entered with the D commit `4673cfa`.

The net `#[test]` count concealed it: 50 -> 51 across that commit reads
as "one test added", but it was +2 (double registration) and -1 (lost
registration). Removing the duplicate and restoring the missing
attribute leaves the count at 51 and every suite total unchanged, which
is exactly why the count is not the instrument. The compiler had already
said so: `duplicate_macro_attributes`, and "function
`shipped_defaults_are_real_money_and_usable` is never used".

Exactly one attribute on each function; nothing else in this commit.
Advisor check D, on the route addendum 13 corrects. The predicate still
hand-split the authority and called a non-numeric port part of the host,
so `https://mint.example:abc` and `https://[::1` both returned true.

The remedy first ordered — `url::Url::parse` — is withdrawn, on two
grounds maxie names: `url` is a DEV-dependency of this crate, so product
code cannot reach it without changing a money-adjacent crate's shipped
dependency set; and the parser would break the contract it was ordered to
enforce, because `Url::parse("http:///x")` yields the host `x` (WHATWG
special schemes skip repeated slashes) and would ADMIT a case this
predicate refuses today. No manifest edit is in this commit.

So the validator stays hand-rolled and becomes correct, in the six steps
of addendum 13 §2: scheme, authority to the first `/`, `?` or `#`, host
after the last `@`, a bracketed IPv6 literal that must close and may carry
only an all-digit port, an unbracketed host with no brackets and at most
one `:` before an all-digit port, and finally a non-empty host with no
whitespace.

Two new admissions with a product reason: a seat's own sidecar mint may
sit on IPv6 loopback, so `http://[::1]:3338` and `https://[::1]` are
admitted. `http://` stays admitted. The 7 previously admitted cases are
all still admitted and all 17 previously refused are all still refused.

Also drops the gratuitous blank line the H commit added after
`mod tests {`, per addendum 13 §4.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the MakePrisms Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant