Route the Authorization: Bearer cashu… redemption failures through the
same failure taxonomy the X-Cashu path already uses (token_already_spent /
invalid_token / mint_error / cashu_error, carried in the error "type"),
with matching statuses, so both redemption paths agree on the
classification for a given failure class instead of introducing a third
parallel code.
- "already spent" -> token_already_spent (400)
- fee/melt failures -> mint_error (422)
- invalid/undecodable token -> invalid_token (400, was 401)
- other expected wallet errors -> cashu_error (400)
- unexpected faults still -> internal_error (500)
Also align the msats<=0 defense-in-depth envelope and note it is now
practically unreachable (credit_balance rejects non-positive amounts).
- Anchor the broad invalid/decode buckets to co-occur with "token" so
internal faults (e.g. "Invalid isoformat string") fall through to 500
instead of masquerading as a 401 token error.
- Map the "estimate fees"/"exceed token amount" wallet errors to the
specific "too small to cover swap fees" message instead of the generic
bucket.
- Stop the X-Cashu redemption path from interpolating raw mint/exception
text into mint_error/cashu_error responses (both duplicated blocks).
- Add tests for the fee-gap mapping and the anchored gate.
A live upstream provider re-finds its own database row in two places —
PPQ.AI's insufficient-balance self-disable and the base
refresh_models_cache — with WHERE base_url == self.base_url AND
api_key == self.api_key. That uses a rotatable secret as a self-handle:
if the row's key rotates under a live object, it can no longer find
itself.
Carry the row's primary key on the instance as db_id, stamped centrally
by from_db_row via a _build_from_row construction hook that subclasses
override, and look the row up with session.get(UpstreamProviderRow,
db_id). This also closes a latent gap where providers built outside the
init path (auto-topup) never received db_id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ui/README.md was still stock create-next-app boilerplate. Replace it with the
real story: the UI is a Next.js static export served by FastAPI from ui_out/, the
two-process dev loop (backend :8000 + `make ui-dev` :3000 with hot reload, auto-
targeting :8000), the build/integration commands, and the cross-origin CORS note.
Also make the "ui_out not found" startup warning actionable — it now points to
`make ui-build` / `make ui-dev` instead of silently saying it skipped serving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WHERE guard now matches on hashed_key, so the UPDATE always affects one
row and result.rowcount is always truthy. Remove the conditional and its
unreachable else branch.
11000 is nutshell's generic, unregistered TransactionError covering many
unrelated failures, so trusting the code alone made any 11000 retryable.
Gate the generic code on the 'not enough inputs' detail text; keep trusting
the registered 11005 (TransactionUnbalanced) on the code alone. Behavior is
unchanged for every existing case; a bare non-shortfall 11000 now surfaces
immediately instead of burning the retry budget.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The same-mint shortcut in swap_to_primary_mint did a same-mint
split(include_fees=True) — which burns the mint's NUT-02 per-proof input
fee — but returned the full token amount, over-crediting the user (the
same bug already fixed for the trusted-mint receive path). It also
skipped DLEQ verification that the trusted path performs.
Extract the shared same-mint redeem into _redeem_same_mint (load mint,
verify DLEQ, split, credit amount - input_fees) and delegate from both
recieve_token and the shortcut, so the two paths can't drift again. This
shortcut is reachable when PRIMARY_MINT_URL is set outside CASHU_MINTS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comment described "21 proofs @ 100 ppk" arithmetic, but the mocked
token has one proof and get_fees_for_proofs is hard-mocked to 3, so the
math wasn't exercised. Describe what the mock actually does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
recieve_token swaps the incoming proofs at the same mint with
include_fees=True (paying the mint's NUT-02 per-proof input fee) but credited
the full face value. On every topup from a fee-charging trusted mint, routstr
over-credited the user by the fee and its own wallet drifted toward insolvency.
Subtract get_fees_for_proofs(proofs) from the credited amount, mirroring the
foreign-mint swap path which already accounts for it. Adds a fee-charging
trusted-mint unit test (credited == face - input_fee).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>