Repository navigation
feat(trade): nostr:// mint support (stacked on #1107) - #1121
Merged
Merged
Conversation
Journal-before-effect single swap into fresh home-owned outputs; NUT-09 restore of the same outputs and identical replay on ambiguity; terminal refused / already_spent / quarantined; idempotent per Y-set; shares the per-mint 100,000-sat lifetime cap with fund; wired into recover/serve and status.
…wn duplicate check
- src/transport.rs: every mint request goes through post/get; http(s) unchanged, nostr:// via maxplayer_core::nostr_mint::NostrMintConnector over --mint-relay (default ditto+wellorder, relay.maxplayer.ai refused via a one-entry deny list). - Asset: canonical nostr://<lowercase npub>; CLI input may be hex/uppercase (from_cli). - Timing: nostr wallet calls bounded at window+margin (40 s), swaps exp<=send_before, abandonment grace 170 s, nostr recovery item budget max(120, 100+40) s. - Admission: nostr legs whose claim/refund swap exceeds 128 I/O or the NIP-44 limit refused. - fund/withdraw refuse nostr:// before any home/journal effect. - tests/nostr.rs: in-process maxplayer-mint sidecar on a local relay.
…-> trade test - receive.rs CDK wallet calls use bounded_for (nostr window, not 15 s). - transport: a definitive nostr refusal reads as 'HTTP 400' like an HTTP NUT error, so receive's refusal classification is transport-independent; everything else stays ambiguous. - tests/nostr.rs funds homes through receive (library and CLI --token-file with hex input).
… to end ambiguous Under TRADE_LAB_SECONDS the taker lock's window is clamped to send_before and can be shorter than the 5 s re-send interval; the lost reply is then recovered by restore, not re-send. The tap records each request's window (exp - created_at); count 1 is allowed only for lab windows <= DEFAULT_RESEND_EVERY. Conservation and production-deadline completion stay asserted.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…C witness Mutation check showed the claim-size estimate could drop the witness and the 128-proof integration test still pass (128 bare 1-sat proofs fit). The unit test finds the largest fitting lock, refuses the next, and proves the same proofs without witnesses would fit.
…irst default --mint-relay Bob's decision (2026-10-09): allow relay.maxplayer.ai for kinds 23410/23411 and list it first, ahead of ditto and wellorder, matching the credits sidecar's default list (parity test against maxplayer-mint MintConfig::effective_relays). The market fence (3410/3411/23412) is unchanged; a new CLI test proves the same host is refused as --relay and accepted as --mint-relay, before any socket opens (never contacts the real relay).
…intRefusal); persist state before reporting it; status shows receive summary Review round 1 findings 1, 2 and 4 on #1119: a 400 without a NUT error body (e.g. a transport expired/bad_request) stays submitted and replays the same outputs; a failed journal write returns the error with the previous state (exit 3); status prints the same receive fields as recover, never the token. fund's cap error names receives. Tests: non-NUT 400, PENDING inputs, partial restore, gross cap with fees, quarantined/refused cap accounting, overlapping Ys, HTLC, CLI exits 1/3/4, failed terminal journal write (lab).
…uarantined holds the cap (review round 1 finding 3)
…uest/rate-limit stay ambiguous Advisor HIGH on #1119, made worse here: transport formatted core's HttpError(400) for wire expired/bad_request as "HTTP 400", which receive's nut_error treats as a definitive refusal, so a token expired by wallet/mint clock skew became permanently refused. nostr_call now formats "HTTP 400" only for the mint's own definitive NUT error; every transport code (expired, bad_request, rate_limited, unsupported, internal, unknown), 413, timeouts and relay failures are ambiguous: attempt retained, identical request replayed later. New unit test covers the classification; new nostr test injects a mint-signed expired reply during receive and proves the attempt stays submitted, a later pass completes it, credited once, exactly one replay.
… nostr mints Conflicts: mint.rs keeps the receive branch's MintRefusal and this branch's transport dispatch (rpc/rpc_until -> transport::post); the HTTP path in transport::post now builds MintRefusal from a parsed 400 NUT body exactly as the receive branch's rpc did. Over nostr, MintRefusal is built only from the mint's own definitive NUT error (nostr_refusal); expired/bad_request/rate_limited/ unsupported/internal/unknown/413/timeout/relay failures stay untyped and ambiguous. SKILL.md takes the receive branch's wording.
This was referenced Oct 10, 2026
…, relay dedupe/loopback, trailing-dot market fence, docs - transport: pin that the sent claim carries no DLEQ (cdk SwapRequest::new strips it) with a really signed, DLEQ-carrying claim at the admission boundary; dedupe mint relays on the parsed URL before the 8-relay cap; one loopback helper (accepts [::1]). - market: production-relay fence ignores a trailing host dot (relay.maxplayer.ai. / %2e). - docs: nostr receive works (README); refused = mint NUT code on either transport; 120 s nostr grace (was misprinted 170); per-call bounds vs enclosing budgets; skill 140 s nostr swap item.
…e), definitive NUT-code allowlist, refused retry, reserved credit until done - F1: refusal and main paths share probe(): restore, NUT-07, restore again on SPENT - F2: MintRefusal::DEFINITIVE mirrors cdk-common 0.17.2 is_definitive_failure; 50000/11002/11003/11004/unknown codes stay submitted and replay - F3: repeating receive with the same token retries a refused attempt with the same journaled outputs after re-checking the cap; recover never replays it - F4: credited rows are RESERVED under the receive id until done is journaled, then released (recover finishes a pending release) - F6 drop journaled token; F7 DLEQ error mapping; F8 Display prints code only; F9 one cap_used helper; README cap sentence
…, double restore) into nostr mints Nostr refusals stay the typed mint::MintRefusal and go through the same MintRefusal::DEFINITIVE allowlist (asserted in transport::tests::only_a_mint_nut_error_is_a_nostr_refusal).
…ted on CI's stable rustc, -D warnings)
… CI clippy -D warnings)
…l (empty retryable set on cdk 0.17.2), journal refusal_code, settled in summary, docs Bob chose (a) limited retry. Traced every allowlisted code through cdk 0.17.2: none can succeed on an identical replay (same inputs, same outputs), so the retryable set is empty. A refused record journals its NUT code; neither recover nor a repeated receive replays it; exit 1 says the token was NOT imported and can be redeemed elsewhere; the cap is freed. Legacy records without a code are final. Also: summary carries settled + refusal_code, recover status counts done-but- unreleased as unresolved, allowlist documented as a strict subset of is_definitive_failure (11003 left out), stale 'token is journaled'/'never swaps twice' wording fixed.
… (review round 2 N1) + nits - receive admission runs transport::check_swap_fits for nostr:// mints before any journal write or cap use; long plain secrets under MAX_TOKEN_BYTES are refused (exit 1) - pre-existing oversized submitted attempt: stays submitted (local 413 is ambiguous, never published, never refused); documented manual resolution + test - 11003 pinned non-definitive over nostr; shared normalized_host for market fence and relay dedupe; ws/wss dedupe comment; README late-lock grace per scheme; advance budget comment
…sal_code) into nostr mints Conflicts in README/commands.md/recovery.md: take #1119's final-refusal wording, keep the nostr transport-code list and the nostr NIP-44 admission refusal; 11003 now listed as ambiguous. SKILL.md trimmed back under 10,000 bytes after the auto-merge.
- recovery.md/README: the legacy oversized nostr submitted row is resolved by redeeming the original token elsewhere; the next recover records already_spent and frees the cap (that row only). - commands.md: list the pre-journal NIP-44 size refusal (exit 1, nothing journaled, cap untouched). - SKILL.md: receive exit 1 includes the pre-journal refusal; restore 'never re-import' on exit 3 (9,998 bytes). - README late-lock line rewrapped; test comment/message wording.
maxie-agent
marked this pull request as ready for review
October 10, 2026 21:54
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1107 (
feat/credit-trade) and on #1119 (feat/credit-trade-receive, receive). Base isfeat/credit-trade; until #1119 merges, this diff also shows #1119's commits, up to25cce15, which is merged in (merge commita40a888; no rebase). The nostr work is the top commits only. Draft. Not for merge without a team member's explicit go-ahead.What
maxplayer-tradecan list, take, claim, refund, and receive on anostr://<npub>mint, such as acrates/maxplayer-mintcredits sidecar. HTTPS mints behave exactly as before.Design
src/transport.rs). Every mint request in the crate goes throughtransport::post/get:mint::rpc(signature unchanged), the/v1/infoand/v1/keysetspreflight reads, and the CDK wallet builder. HTTP(S) mints keep the rawreqwestrequests byte for byte. Anostr://mint uses the reviewed core connectormaxplayer_core::nostr_mint::NostrMintConnector: one kind-23410 request and one 23411 reply per call, an identical re-send on a lost reply, and a mint-enforcedexp. A path dependency onmaxplayer-core(featureswallet) works exactly ascrates/maxplayer-mintdoes. There is no core or mint change;git diff feat/credit-trade -- crates/maxplayer-core crates/maxplayer-mintis empty. CDK wallets for nostr mints are built with.client(connector).use_http_subscription(), as in corebuild_wallet_with_relays.receive(feat(trade): receive — import Cashu tokens into a trade home (stacked on #1107) #1119) gets nostr through the same helpers. Its only changes arebounded→bounded_forand the refusal wording below.nostr://<lowercase bech32 npub>, with no slash, path, query or fragment.Asset::new/validate(lots, peers, tokens) accept only that form, the same form the market site accepts.Asset::from_clialso accepts a 64-hex key, uppercase, or one trailing slash, and canonicalizes it.--mint-relay(max 8;wss://, orws://only on loopback), defaulting towss://relay.maxplayer.ai,wss://relay.ditto.pub,wss://nostr-pub.wellorder.netin that order (transport::default_mint_relays=DEFAULT_MINT_RELAY+ coreFALLBACK_RELAYS). That is exactly the list and order a default credits sidecar listens on (maxplayer-mintMintConfig::effective_relays), and a test asserts the two are equal. Mint relays carry kinds 23410/23411 only, and market relays never carry mint traffic (a relay tap asserts this in tests).relay.maxplayer.ai: Bob's decision (2026-10-09). It carries mint traffic (23410/23411) only. The mint-relay denylist is gone. The market fence inmarket.rs(3410/3411/23412,--relay) is unchanged and still refuses it.production_relay_carries_mint_traffic_but_never_market_trafficruns the real CLI with the same host (plain andRELAY.MAXPLAYER.AI/) as both--relayand--mint-relay: market is refused withproduction relay forbidden, and mint alone is accepted. Both checks run before any socket opens. Understrace -e connectthe two CLI invocations made 0 non-Unix connects, so no test writes to the real relay.wallet::bounded_for(mint, …): 15 s for HTTP (unchanged), and window 30 s +OUTER_MARGIN10 s = 40 s for nostr. All 29 trade call sites and all 8 in receive use it. The oldboundedis kept for compatibility.expis at or beforesend_before. With under 2 s left it is not sent at all.MAX_CLOCK_SKEW_SECS60 + 20. HTTP stays at 60 s.states()is not cut. The 60 s inbound message path is unchanged: it can cut a nostr claim, butclaimingand the preimage are saved first and the next tick claims with the full budget (same argument as feat(trade): standalone fixed-lot Cashu token trading (credit trading prototype) #1107). Any cut is ambiguous: the journaled attempt is restored, never re-sent with new outputs, and never treated as failed.mint::validate) refuses an incoming nostr lock whose HTLC claim would exceed either limit (each input carries a preimage plus a signature), before any network call. Own locks are checked the same way before the attempt is journaled, both for the lock swap and for its later claim or refund.money::fund,withdraw_bounded, quote GET, melt POST) refuse too.5deccae, then merged with feat(trade): receive — import Cashu tokens into a trade home (stacked on #1107) #1119's typedmint::MintRefusalina40a888). Before,nostr_callformatted core'sHttpError(400)as "HTTP 400". Core uses that for the wire codesexpiredandbad_request, which mean "not executed".receivetook "HTTP 400" as a final refusal, so a token that expired because of clock skew became permanentlyrefused. Nowtransportbuilds aMintRefusalin two cases only: over HTTP, from a parsed 400 NUT body (as feat(trade): receive — import Cashu tokens into a trade home (stacked on #1107) #1119'srpcdid); over nostr, from the mint's own definitive numeric NUT error (nostr_refusal). Every transport code (expired,bad_request,rate_limited,unsupported,internal, unknown), plus 413, timeouts and relay failures, stays untyped and ambiguous: the attempt is kept, and the identical request is replayed later. No error is formatted as "HTTP 400" text any more.Test map (
tests/nostr.rs: real sidecar in-process on a local relay; no real mints)Safety is asserted before return values in every test.
receive --token-file(CLI, hex input) → trade nostr↔httpsnostr_issue_receive_then_https_trade_completes_on_mint_relays_only(also: no mint kinds on market relays and vice versa)nostr_nostr_trade_completes_through_lost_swap_replies(each swap's first reply dropped; 4 swaps, each re-sent identically; conservation)nostr_taker_timed_refund(lab)nostr_maker_refund_survives_lost_reply(lab)nostr_counterparty_unreachable_refund_still_lands(lab; sidecar stopped, relay up = black hole)nostr_preflight_refuses_missing_nut_and_cli_canonicalizes(NUT-14 off: library,listcreates no listing, CLI)relays.rs::production_relay_carries_mint_traffic_but_never_market_traffic(real CLI, no socket opened),relays.rs::production_relay_is_forbidden(market fence, library),nostr.rs::default_mint_relays_match_the_sidecar_defaults, unitmint_relays_fenced_and_bounded(default order)nostr_dleq_verified_on_lock_admission(forged and missing DLEQ refused)nostr_lock_with_too_many_proofs_refused_at_admission(128 × 1-sat HTLC lock refused before any mint request)nostr_fund_and_withdraw_refused_before_any_intent(library + CLI; the home is never created)nostr_slow_claim_inside_window_completes(25 s reply completes in the same call) andnostr_claim_past_window_stays_ambiguous_then_restores(36 s reply: retained, not abandoned, restored, never a second swap)exp ≤ send_beforenostr_swap_exp_never_passes_send_beforeexpiredduring receive is not a refusalnostr_expired_swap_reply_during_receive_stays_submitted_then_completes(tap answers the first receive swap with a mint-signedexpiredreply and the sidecar never sees it; attempt stayssubmitted, nothing credited; a laterrecovercompletes it, credited exactly once, exactly one replay) and unitonly_a_mint_nut_error_is_a_nostr_refusalnostr_mint_canonical_form,mint_relays_fenced_and_bounded,nostr_grace_covers_window_exp_and_skew,nostr_claim_size_boundVerification at head
a40a888Local (
--locked,--test-threads=1, same commands and clippy-Alist as the CImaxplayer-tradejob):cargo fmt --check: ok. Clippy default + lab--all-targets --no-deps -D warnings: ok.TRADE_LAB_SECONDS=1 --features lab): 183 passed, 0 failed. lib 31, cli 5, e2e 11, money 17, nostr 14, pinned_quote_recovery 1, receive 23, relays 8, review_regressions 46, round5 26, round7 1.a40a888: 9/9 jobs green, includingmaxplayer-trade (fmt, clippy, default + lab tests). No reruns.The lost-reply test: what it requires, and where it bends
nostr_nostr_trade_completes_through_lost_swap_replies(tests/nostr.rs:502) drops the first reply to every swap (two locks, two claims), so each one needs the identical re-send. It then runs both sides to a terminal state.At production deadlines it must complete with exact balances. Lines 538–549: if both sides are not
complete,assert!(lab, "production deadlines must complete through lost replies")fails.labis true only with--features labandTRADE_LAB_SECONDSset (line 515). Completion requiresheld == [96, 32, 24, 104](line 539) and exactly 4 swap requests (line 541), so a lost reply never becomes a new swap. The default suite (locally and in CI'smaxplayer-tradejob) runs withlabfalse, so there the test passes only by completing.With the lab's short locks (16 s short lock, 48 s long, 2 s cutoff) it may instead end fully refunded. That isn't required, only allowed. A 5 s re-send per swap can push the claim past the 16 s lock, and then refunding is the correct, safe outcome. To be plain about the history: the test as first written required completion in every mode. The first local lab run failed it with both sides
refundedand every sat back. I then added this lab-only refunded branch, before the first push (it is already in46d7084), so no CI run ever saw the stricter version. The production requirement was never loosened.Which assertion proves no money is lost in the refunded branch: lines 550–554,
heldis[maker@n1, taker@n1, maker@n2, taker@n2], the wallets' unspent balances. Each side must hold exactly the 128 sats it funded, on its own mint, with the counterparty holding 0. The conservation check at line 523 (held[0]+held[1] == 128 && held[2]+held[3] == 128, run on every branch) alone wouldn't be enough, because it would allow coins to move to the wrong party. The exact tuple rules that out. A half-refunded or stuck end (e.g.refunded/complete, or any state other than bothrefunded) fails the test.The second relaxation (
537e52c) is narrower, and it is lab-only too. Line 535 normally requires every swap to have been published at least twice (count >= 2, the identical re-send). In lab, a swap whose deadline-clamped window (exp - created_at) is at most one 5 s re-send interval may havecount == 1. Its call ended ambiguous before a re-send was due, and recovery restored it instead of publishing anything new. A full lab run hit this once, with conservation holding. Outside lab the exception doesn't apply. Its limits:Mutation check
17 mutants, each run against its named test in a separate worktree and
CARGO_TARGET_DIR, both deleted afterwards. All 17 are killed by the final head.expnot clamped tosend_before; no 2 s minimum window; nostr RPC cut at 20 s; nostr wallet bound back to 15 s; nostr grace back to 60 s; restore skipped on recovery; CLI fund/withdraw gate removed; library withdraw gate removed; library fund gate removed; DLEQ not verified at admission;(now intended, see below); non-canonical nostr URL accepted; NUT-14 not checked in preflight; maker own-mint refund skipped; taker timed refund never runs.relay.maxplayer.aiallowedtransport::tests::nostr_claim_size_boundinae9844d, which finds the largest fitting lock, refuses the next, and shows the same proofs without witnesses would fit. Two mutants first missed their anchors because of formatting; after correcting the anchors, both were killed.347ad88), the "relay.maxplayer.aiallowed" mutant describes intended behaviour and is retired. Three new mutants, run against their named tests, are all killed: market fence removed (production_relay_carries_mint_traffic_but_never_market_traffic), mint relay denied again (same test), default order changed (default_mint_relays_match_the_sidecar_defaults).5deccae,a40a888): five mutants, all killed. The old "any 400 is a refusal" rule is killed both by the expired-receive integration test and by the unit test; "never a refusal" by the unit test; "typed refusal for every nostr error" by the expired-receive test; HTTP 400 NUT body left untyped byreceive.rs(definitive_refusal_is_terminal_refused,cli_exit_codes_for_journaled_refused_and_quarantined). Each mutant was run in place on a clean tree and reverted, with no extra worktree.Follow-ups
: done in feat(trade): receive — import Cashu tokens into a trade home (stacked on #1107) #1119. This PR makes it work onreceive <token>nostr://mints and tests issue → receive → trade.: decided by Bob (2026-10-09) and done inrelay.maxplayer.aifor mint traffic347ad88: mint traffic only, first default--mint-relay. Market fence unchanged.receive::RECOVERY_ITEM_SECONDSstays at 120 s. For nostr that is still more than 2 × 40 s per call; a cut leaves the receipt retained (ambiguous) for the next pass.Not verified
docs/specs/seller-credits.md§6 records that the relay passes them for the sidecar (NIP-42, fresh keys); the trade client uses the same core connector with automatic NIP-42 auth, but this PR did not test against the live relay. Every nostr test uses an in-process sidecar on a loopbacknostr-relay-builderrelay. The default relays are not exercised here.Review round 1 (reviews of
a40a888; fixes2ab62c2, merge of #1119a5b6539in776896e, CI fixf0c0345, merge of #1119059ebebin707f8e3)Reviewers: cashu-rust-dev (approve-with-nits) and advisor (request-changes). Both are independent of the author. Every finding below was checked against the code at
a40a888before anything was changed. Fixes were made by the author's agent; Bob approved this pass at 02:07 UTC. Author-led work is not an independent re-review.How the DLEQ disagreement was settled (advisor MEDIUM 1 vs cashu-rust-dev Q4)
Rejected. The bound correctly leaves DLEQ out, because DLEQ is never sent.
redeem_inner(mint.rs:452-457) does keep the counterparty's DLEQ on the journaledAttempt.inputs.execute→SwapRequest::new(a.inputs.clone(), messages)(mint.rs:552).SwapRequest::newbuildsinputs: inputs.without_dleqs()(nut03.rs:52-56), andwithout_dleqssetsp.dleq = Noneon every proof (nut00/mod.rs:82-90).post_swap→to_body(&request)), and checks the plaintext againstMAX_PLAINTEXT_BYTES= 65,535 (mint_wire.rs:48). The sidecar drops any plaintext over that same limit (maxplayer-mint/src/server.rs).check_claim_fitsclearing DLEQ (it also goes throughSwapRequest::new) matches what is actually sent.New boundary test
transport::tests::nostr_claim_bound_covers_real_signed_claim_with_dleqcovers locks of 1-sat proofs that carry DLEQ, for every count from 1 up to the largest one admission accepts:add_preimage+sign_p2pk, DLEQ left on each proof) with binary-split outputs;dleq;If a future cdk version kept DLEQ on swap inputs, the no-
dleqassertion would fail and the bound would have to count it.Findings
2ab62c2,nostr_claim_bound_covers_real_signed_claim_with_dleq. Mutation: claim witness without its signature in the estimate → fails "admitted 79-proof lock has a 66244-byte claim over the NIP-44 limit".2ab62c2: README Receive section now points to the nostr transport and its tests. Also audited README, SKILL.md and references for other stale nostr text (A3, A4 and the nits below).2ab62c2: SKILL.md says "nostr://swap: 140 s". It stays under the 10,000-byte SKILL.md limit enforced byskill_commands_and_flags_match_binary_help.2ab62c2: README,references/commands.mdandreferences/recovery.mdnow say a refusal is the mint's numeric NUT code on either transport, only after an empty restore and UNSPENT inputs. Nostrexpired/bad_request/rate_limited/ timeout / 413 →submitted(exit 3).NOSTR_ABANDON_GRACE_SECONDS.nostr://.nostr://<lowercase npub>for the credits mint.tokio::time::timeout. A drop leaves the swap exactly as it was before the call, and no caller treats it as a definitive failure:-
executehas no terminal path on a timeout;-
receivemarksRefusedonly on a typedMintRefusalplus an empty restore plus UNSPENT inputs;- lock failure becomes
lock_reconciling/ release only afterabandoned(send_before + 120 s on the mint clock, plus a fresh not-landed check);- the next tick restores the same outputs and replays the identical request, whose
exp ≤ send_beforeis enforced by the mint.The cost is liveness, not money. The "never cut short" claims in
wallet.rs/coordinator.rsdocs and README were reworded to "per call".relay.maxplayer.ai./%2e)2ab62c2:market.rstrims the trailing dot before comparing.tests/relays.rs::production_relay_is_forbiddennow covers the plain, uppercase, trailing-dot,%2eand port/path spellings. Mutation (dot not trimmed) → fails "SAFETY: market traffic to wss://relay.maxplayer.ai./ refused". An IP literal or CNAME still bypasses the hostname fence: deferred. Relays are operator-supplied, and refusing by IP / NIP-11 identity is a separate design.loopback_onlyrejects[::1]2ab62c2: dedupe on the parsed URL (lowercase host, no trailing dot or slash, default port), then apply the 8-relay cap. A singleloopbackhelper is shared. Testmint_relays_fenced_and_bounded; mutations: raw-string dedupe → fails; old loopback check → fails on[::1].maxplayer-core features=["wallet"](libgit2, hyper server, daemon code, rc path deps)- one cdk/cashu 0.17.2, one nostr-sdk, one libsqlite3-sys;
-
maxplayer-mintis a dev-dependency only;- the trade binary never calls the extra code.
The path dependency means a core connector change alters this crate without a diff here. That is mitigated because CI has no path filters, so the trade job runs on every PR. Follow-up: a narrow core
nostr-mintfeature exposing onlynostr_mint+mint_wire.--mint-relayis not persisted across restartsnostr://trade". If the flag is missing, calls are ambiguous, never terminal. Follow-up: persist the relay set in the home and warn on mismatch.claim_outputsassumes binary denominationsUnitConfig::default()). Only a third-partynostr://mint with non-binary amounts would differ, and the 512 B margin covers about 4 extra outputs. Follow-up: count with the keyset'sFeeAndAmounts.nostr_nut_refusalis a denylist over cdk variantsErrorCode::Nut. Today's mapping is pinned byonly_a_mint_nut_error_is_a_nostr_refusal. Follow-up: core exposes the NUT/named distinction and this crate allowlists on it.origin/feat/credit-trade-receivea5b6539(definitive NUT-code allowlist, double restore)776896e(merge, no rebase)mint::MintRefusalbuilt intransport::nostr_call. Receive gates them withmint::definitive_refusal(MintRefusal::DEFINITIVE), the same as HTTPS. New assertions inonly_a_mint_nut_error_is_a_nostr_refusalbuild the error exactly asnostr_calldoes: 11001 is definitive; 11002, 11004, 50000, unknown codes andexpiredare not; anything definitive must be on the allowlist. Mutation: dropping both the allowlist and the transport cdk gate fails ("token pending stays ambiguous"). Dropping only the allowlist survives, because over nostr the cdkis_definitive_failuregate already excludes those codes (defense in depth, not a gap). Docs were merged to cover both transports.AtomicU64::fetch_update(added in #1119 test support) is deprecated on CI's current stable rustc, so-D warningsfails the trade clippy job (also red on #1119 ata5b6539)f0c0345(helper), then #1119 fixed the same line itself (059ebeb); merge707f8e3takes #1119's version, so both branches carry identical test support.exp = send_before + 1; 60 s message path may always deferVerification
After the merge (
776896e, local,--locked,--test-threads=1):TRADE_LAB_SECONDS=1): 193 passed.At
707f8e3(after merging059ebeb, a test-support-only change): clippy (default + lab) is clean and thereceivesuite passed 23. The full suites were not rerun locally for this one-file merge; CI covers them.Pre-merge, at
2ab62c2, local,--locked,--test-threads=1:cargo fmt --check: clean.--features lab, with-D warningsand the CI allow list: clean.TRADE_LAB_SECONDS=1): 184 passed, 0 failed.Still unverified
expenforcement and reply replay were not re-read in this round.Review round 2 (reviews of
707f8e3; fixes39d892b, merge of #1119 round 3e4a0059in6aceee5)Reviewers: the CRD round-2 re-review (approve-with-nits) and the advisor re-review (approve-with-nits). Both are independent of the author. Fixes are author-led. Bob approved this pass at 15:02 UTC on 2026-10-10.
MAX_TOKEN_BYTES) stayssubmittedforever and holds the capreceive.rsadmission runstransport::check_swap_fitson the exact swap body (sharedswap_request) after the outputs are built and BEFOREj.put. The token is refused with exit 1: nothing journaled, cap untouched, no swap published. Testnostr_oversized_receive_refused_before_journal: a 128×400-character-secret token is refused, then a 128×64-character token completes.submittedrecordsubmitted(exit 3). Core's local 413 is untyped, so it is neverrefused, and it is never published; the inputs stay with the token holder. It holds its cap until resolved by hand (README receive section,recovery.md). This could only come from unreleased builds of this branch. Testnostr_preexisting_oversized_receive_is_never_sent. No automatic terminal state: marking itrefusedwould need a non-mint classifier, which conflicts with "refusals only from typedMintRefusal".transport.rsonly_a_mint_nut_error_is_a_nostr_refusalloop now includes 11003 (core-definitive, not allowlisted)relay_keykeeps ws/wss distinct; add a commenttransport.rsrelay_keydoctransport::normalized_host, used bymarket.rsfence andrelay_key;production_relay_is_forbiddenstill passesabandon_grace_seconds60 s HTTPS / 120 s nostrstates()coordinator.rsADVANCE_BUDGET_SECONDSdoc: 140 s item andNOSTR_OUTER40 s for nostrSwapRequestwould skip DLEQ strippingSwapRequest::new; nothing journals a wholeSwapRequestmint.rs:552citationMINT_RELAYSunit testMerge of
e4a0059(#1119 round 3). Doc conflicts in README,commands.mdandrecovery.mdwere resolved by taking #1119's final-refusal andrefusal_codewording, keeping the nostr transport-code list, and adding 11003 to the ambiguous list. The auto-merge pushedSKILL.mdto 10,024 bytes, so I trimmed it back to 9,996 (tests/cli.rsrequires < 10,000). Nostr refusals still reachrefusedonly throughmint::definitive_refusalon a typedMintRefusal: the receive gate is unchanged apart from journalingrefusal_code.Local gates at
6aceee5(all--locked, CI clippy allow list): fmt OK; clippy default and lab clean; default suite 143 passed, 0 failed; lab suite (TRADE_LAB_SECONDS=1) 195 passed, 0 failed. One local default run at this head failedmoney::failed_preflight_refuses_funding_before_intentonce, while sibling worktrees were running heavy parallel test jobs. That test and its code are untouched since707f8e3. It passed 5/5 in isolation and in the full default rerun, and passed in CI run https://github.com/MakePrisms/maxplayerai/actions/runs/38066033628 (all jobs green at6aceee5, including maxplayer-trade; no reruns needed).Mutation checks (disposable worktree + separate target, both deleted afterwards). Baselines pass. Each mutant fails at its SAFETY assertion:
check_swap_fits→nostr_oversized_receive_refused_before_journalfails at "SAFETY: an unsendable nostr receive must be refused at admission" (the record wentsubmitted);refused→nostr_preexisting_oversized_receive_is_never_sentfails at the state assertion;MintRefusal::DEFINITIVE→ the unit test fails at "SAFETY: nostr NUT 11003 stays ambiguous".Still unverified: no real mint or relay was used. The fix was not checked against a non-sidecar nostr mint's size limits. The oversized-record path was tested with a synthesized journal record, not one written by an older binary.
Review round 3 (reviews of
6aceee5; fixesa8dd75d)Reviewers: CRD round-3 re-review (approve-with-nits) and advisor round-3 re-review (approve-with-nits), both independent of the author. Fixes are author-led. Bob approved this pass at 19:48 UTC on 2026-10-10.
origin/feat/credit-trade-receiveis stille4a0059, so there was nothing to merge; no rebase, no force-push.recovery.mdsubmittedrow and README receive section: for that legacy oversized nostr row only, redeem the original token elsewhere (the swap never left the process); the nextrecoversees SPENT inputs with none of our outputs, recordsalready_spentand frees the cap. Every othersubmittedreceive must still never be re-imported. Test comment/message innostr_preexisting_oversized_receive_is_never_sentreworded.commands.mdpre-journal refusal list adds thenostr://swap that does not fit one NIP-44 request (exit 1, nothing journaled, cap untouched).SKILL.mdreceive: exit 1 includes the pre-journal refusal (nostr NIP-44 size); exit 3 saysrecover, never re-import.SKILL.mdis 9,998 bytes (tests/cli.rs< 10,000).already_spent+ cap(0,0)probe → Spent → already_spentpath is already covered for HTTPS receives; adding a nostr-sidecar variant is a test-only follow-up.submittedand replays identically. MakingRECOVERY_ITEM_SECONDSnostr-aware changes the recovery timing on a money path and deserves its own review.nostr_nut_refusal(&nut(11003))!definitivefor 11003) is pinned; the typing assertion adds no safety.Local gates at
a8dd75d(docs/test-comment only vs6aceee5; all--locked, CI clippy allow list): fmt OK; clippy default and lab clean; default suite 143 passed, 0 failed; lab suite (TRADE_LAB_SECONDS=1) 195 passed, 0 failed. (The first lab run was killed with its shell session mid-suite, with no failure logged; the detached rerun completed.)Still unverified: no real mint or relay; the legacy-row resolution was traced in code, not executed.