From 54fefc01496ad0a8d7eda536f3ad50c618b1d6bc Mon Sep 17 00:00:00 2001 From: "worker:mobee-crossmint" Date: Mon, 27 Jul 2026 16:06:02 -0700 Subject: [PATCH 1/4] crossmint: a smoke script that runs, and states what it cannot prove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smoke script's command surface had never been executed. Against the real binary every invocation returns rc=1: `budget`, `config` and `job` are not commands at all; `--home` is a post-subcommand flag on `wallet` alone; `wallet balances` is `balance` and has no `--json`; `buyer restart` does not exist; and the binary is not at ./target (CARGO_TARGET_DIR is redirected). A script in that state does not fail at step 0 — it fails partway, and on the real-money path that means after the melt. The script now targets the surface the binary has, and is staged so the external unknowns are measured before our own code is involved: --self-test parsers against fixtures. The `status=needs_payment` branch only ever appears on a non-test mint, so fixtures are the only coverage available before real sats. --dry-run testnut: gates, home discipline, env config, balance math, and that a melt which cannot route returns non-zero — the property every `|| die` guarding a real melt depends on. --stage-a the hop by hand in three wallet commands. Answers what no hermetic test can: does LN route source -> target, and what does it cost. Ends in a fee-fit verdict against the cap. --stage-b refuses, naming what it still needs, rather than pretending. Safety is structural rather than procedural. Config is supplied entirely by environment, and env overrides are never persisted: a home booted with MOBEE_ALLOW_REAL_MINTS=true still reads `allow_real_mints = false` on disk, so the real-mint fence is process-scoped and cannot be left open. Bootstrap does write into MOBEE_HOME, and an unset MOBEE_HOME resolves to ~/.mobee, so every invocation is funnelled through one wrapper that refuses any directory this run did not itself create and mark. Co-Authored-By: Claude Opus 5 --- scripts/crossmint-smoke.sh | 466 +++++++++++++++++++++++++++---------- 1 file changed, 337 insertions(+), 129 deletions(-) diff --git a/scripts/crossmint-smoke.sh b/scripts/crossmint-smoke.sh index cdbe2ce3..36a6bb19 100755 --- a/scripts/crossmint-smoke.sh +++ b/scripts/crossmint-smoke.sh @@ -3,146 +3,354 @@ # Live cross-mint hop smoke — a small REAL-SATS trade between two different mints. # # ┌───────────────────────────────────────────────────────────────────────────────────────────┐ -# │ THIS SCRIPT SPENDS REAL MONEY AND REQUIRES A MONEY-GATE CONFIG CHANGE. │ -# │ It refuses to run without an explicit authorization token naming the exact amount and │ -# │ both mints, so it cannot be started by an agent, a retry, or a stray shell history entry. │ +# │ THE REAL STAGES SPEND REAL MONEY. They refuse to run without an authorization token naming │ +# │ the exact amount and both mints, so they cannot be started by an agent, a retry, or a stray │ +# │ shell history entry. │ # └───────────────────────────────────────────────────────────────────────────────────────────┘ # -# Why this exists at all: test ecash structurally CANNOT hop. Fake mints cannot pay each other's -# invoices over real Lightning, so the hermetic teeth prove control flow, the journal, budget -# accounting and recovery — but not real LN routing between two mints. This is the only thing that -# proves that leg, and it is the reason the slice ships with that caveat stated rather than hidden. +# Why this exists: test ecash structurally CANNOT hop. A test mint auto-settles its own quotes and +# cannot pay another mint's invoice over real Lightning — measured, not assumed: on testnut a melt +# of a freshly issued invoice returns `wallet error: Payment failed` (rc=2). So the hermetic teeth +# prove control flow, the journal, budget accounting and recovery; only real sats prove LN routing +# between two mints. That is the caveat the slice ships with, stated rather than hidden. # -# Usage (after authorization): -# CROSSMINT_SMOKE_AUTH="::" ./scripts/crossmint-smoke.sh +# ── Stages ────────────────────────────────────────────────────────────────────────────────────── +# --self-test parsers only. No network, no home, no money. +# --dry-run testnut. Proves the gates, home discipline, env config, balance math, and that a +# failed melt halts the script. Cannot prove a SUCCESSFUL melt (see above). +# --stage-a REAL SATS. Routing + fee probe: the hop performed by hand in three wallet +# commands — no daemon, no relay, no seller, no hop code. Answers the only question +# hermetic tests cannot: does LN route source -> target, and what does it cost? +# --stage-b REAL SATS. The full trade through the real pay path. # -# The token must match the three values below exactly. Change the values, and the token stops -# matching — which is the point: authorization is for one specific spend, not for the script. +# Stage A before Stage B is deliberate. The unknowns in A are EXTERNAL (two mints, LN routing, +# fees); the unknowns in B are OURS. Run together, a failure tells you neither. Run in order, an +# A-failure means the mint pair and a B-failure means our code. +# +# Usage: +# CROSSMINT_SMOKE_AUTH="::" ./scripts/crossmint-smoke.sh --stage-a +# +# The token must match the configured values exactly. Change the values and the token stops +# matching — authorization is for one specific spend, not for the script. set -euo pipefail # ── The authorized spend ──────────────────────────────────────────────────────────────────────── -# Fill these in from the authorization, then hand the operator the matching CROSSMINT_SMOKE_AUTH. -AMOUNT_SATS="${AMOUNT_SATS:-}" # what the seller receives (the buyer pays this plus hop fees) -SOURCE_MINT="${SOURCE_MINT:-}" # the mint the buyer holds sats at -TARGET_MINT="${TARGET_MINT:-}" # the mint the seller accepts — MUST differ from SOURCE_MINT -BUYER_HOME="${BUYER_HOME:-}" # buyer home whose config gets allow_real_mints -SELLER_HOME="${SELLER_HOME:-}" # seller home advertising TARGET_MINT +DELIVERY_SATS="${DELIVERY_SATS:-21}" # what the seller receives; the buyer pays this plus hop fees +PROBE_SATS="${PROBE_SATS:-5}" # Stage A probe. Small: its job is to measure, not to deliver +EXPOSURE_CAP_SATS="${EXPOSURE_CAP_SATS:-50}" +SOURCE_MINT="${SOURCE_MINT:-}" # the mint the buyer holds sats at +TARGET_MINT="${TARGET_MINT:-}" # the mint the seller accepts — MUST differ from SOURCE_MINT -MOBEE="${MOBEE:-./target/release/mobee}" +MOBEE="${MOBEE:-/srv/forge/workspaces/.crossmint-target/release/mobee}" +RUN_DIR="${RUN_DIR:-}" + +# The marker that makes a directory a legitimate target. Bootstrap WRITES into MOBEE_HOME, and an +# unset MOBEE_HOME falls back to ~/.mobee — a real home with a real key and wallet. So no mobee +# command in this script is reachable except through mobee_at(), which refuses any directory this +# run did not itself create and mark. +MARKER=".crossmint-smoke-throwaway" die() { echo "crossmint-smoke: $*" >&2; exit 1; } +say() { echo "$*"; } +rule() { echo "--- $* ---"; } + +# ── Parsers ───────────────────────────────────────────────────────────────────────────────────── +# Factored out and self-tested because the live formats they consume are the ones the dry run +# CANNOT produce: testnut auto-settles, so `status=needs_payment` never appears against a test +# mint. Proving them against fixtures is the only honest coverage available before real sats. + +# `mint= role= balance_sats=` lines, then `total_sats=`. +parse_balance_for_mint() { # + printf '%s\n' "$1" | command grep -F "mint=$2 " | command sed -n 's/.*balance_sats=\([0-9]*\).*/\1/p' | command head -1 +} +# `status=needs_payment amount_sats= mint= quote_id= (...)` +parse_quote_id() { printf '%s\n' "$1" | command sed -n 's/.*quote_id=\([A-Za-z0-9-]*\).*/\1/p' | command head -1; } +# `paid_sats= fee_sats= balance_sats= mint=` +parse_field() { printf '%s\n' "$1" | command sed -n "s/.*$2=\([0-9]*\).*/\1/p" | command head -1; } + +self_test() { + local fail=0 got + check() { #