Fix: Standardize error codes - #1129
Conversation
The clear and blind auth errors shipped codes in the 80000/81000 range, but NUT-21 and NUT-22 specify 30001/30002 and 31001-31004 respectively. Clients dispatching on the specified codes never matched. Remap all six classes to their specified codes. Error messages are unchanged, so callers matching on `detail` are unaffected. Add a parametrized test pinning each class to its specified code, and assert the code on the four raise paths that already had coverage. Closes cashubtc#769
TransactionUnitError duplicated TransactionMultipleUnitsError on 11009; remove it and raise the latter at its single call site. SecretTooLongError and WitnessTooLongError sat on 11003 and 11004, which the NUTs assign to "outputs already signed" and "outputs are pending". No NUT code covers input length limits, so both fall back to the generic 11000. Classes and messages are unchanged.
Minting disabled, quote pending, invoice already paid and quote expired all surfaced as generic TransactionError (11000) or NotAllowedError (10000), neither of which appears in the NUT error code table. Add MintingDisabledError (20003), QuotePendingError (20005), InvoiceAlreadyPaidError (20006) and QuoteExpiredError (20007), and raise them at the eight sites that already detect these conditions. Each site keeps its existing message, so only the code changes. Melt-disabled keeps NotAllowedError: 20003 is mint-only per NUT-04.
Amountless invoices, amount mismatches and unsupported units surfaced as generic TransactionError (11000) or NotAllowedError (10000), neither of which appears in the NUT error code table. Add AmountlessInvoiceNotSupportedError (11011), AmountMismatchError (11012) and UnitNotSupportedError (11013), and raise them at the five sites that already detect these conditions. Each site keeps its existing message, so only the code changes. The method/unit backend check in _verify_and_get_unit_method keeps NotAllowedError: 11013 covers the unit, not the method.
There was a problem hiding this comment.
Pull request overview
This PR standardizes and expands error-code usage across the mint by aligning authentication errors with NUT-21/NUT-22 and by introducing more specific error types for quote lifecycle and transaction validation paths.
Changes:
- Update authentication error codes to match NUT-21/NUT-22 (300xx / 310xx).
- Introduce/replace several mint/transaction errors with more specific exception classes and codes (e.g., quote lifecycle, unit support, invoice/amount checks).
- Update mint tests to assert specific error codes and exception types.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
cashu/core/errors.py |
Adds new error classes and adjusts multiple error codes, including auth codes and new quote/transaction errors. |
cashu/mint/ledger.py |
Switches several mint/melt code paths to raise the new standardized errors. |
cashu/mint/verification.py |
Replaces unit-related exceptions with standardized transaction errors. |
cashu/mint/db/write.py |
Replaces generic transaction errors with quote-lifecycle errors when setting/storing quote pending states. |
tests/mint/test_mint_verification.py |
Adds assertions for standardized transaction error codes and updates expected exception types. |
tests/mint/test_mint_operations.py |
Adds coverage asserting quote lifecycle error codes and behavior for disabled/pending cases. |
tests/mint/test_mint_melt.py |
Updates expected exception type/code for duplicate checking-id pending behavior. |
tests/mint/test_mint_auth_server_unit.py |
Adds tests ensuring auth error codes match NUT-21/NUT-22 and asserts raised codes in auth flows. |
Suppressed comments (2)
cashu/core/errors.py:88
WitnessTooLongErroralso setscode = 11000(same as the baseTransactionError), which removes any ability for clients to reliably branch on this specific failure and is inconsistent with most otherTransactionErrorsubclasses having their own codes. Consider giving this error a dedicated unused code and updating the relevant test assertions accordingly.
class WitnessTooLongError(TransactionError):
code = 11000
def __init__(self, detail="witness too long"):
super().__init__(detail, code=self.code)
cashu/mint/db/write.py:455
- Same as above:
_store_melt_quoteraisesQuotePendingErroreven when the conflicting quote is alreadypaid. IfInvoiceAlreadyPaidError(20006) is the intended “already paid” signal elsewhere, consider using it here for the paid case and reservingQuotePendingError(20005) for pending-only conflicts.
quote.state in [MeltQuoteState.pending, MeltQuoteState.paid]
for quote in quotes_db
]
):
raise QuotePendingError("Melt quote already paid or pending.")
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1129 +/- ##
==========================================
+ Coverage 74.85% 74.90% +0.05%
==========================================
Files 112 112
Lines 12589 12631 +42
==========================================
+ Hits 9423 9461 +38
- Misses 3166 3170 +4 ☔ View full report in Codecov by Harness. |
DbWriteHelper re-checks quote state under the row lock, and that check is the authoritative one -- the matching check in ledger.py is an optimistic pre-check. Both raised the same conditions, but the locked path returned generic TransactionError (11000), so a client losing a race got 11000 where the winner got the spec code. Raise QuotePendingError (20005), QuoteNotPaidError (20001), QuoteAlreadyIssuedError (20002) and InvoiceAlreadyPaidError (20006) at the guards. The two melt guards tested for paid and pending together, so they are split to report the state the client actually hit; the same set of states raises, with messages unchanged. QuoteNotPaidError and QuoteAlreadyIssuedError take an optional detail so the guards keep their quote_id in the message.
1547a3d to
dcefeb1
Compare
callebtc
left a comment
There was a problem hiding this comment.
Review findings:
- High: A repeat melt request reaches Ledger._prepare_melt before the new locked guard. Its
if not melt_quote.unpaidreturns generic 11000 for both PAID and PENDING, rather than the documented 20006 and 20005. Please split that pre-check too.
The two inline comments cover the remaining issued-quote paths.
Split the melt pre-check in _prepare_melt so a repeat melt reports 20006 or 20005 instead of a generic 11000, add the missing issued branch to _set_mint_quote_pending, and convert the two internal-melt issued paths.
be1bc5f to
9583d96
Compare
title: "NPC wallet marks unpaid/unissued quotes as ISSUED because it dispatches on the wrong mint error code (11000 instead of 20002)"
|
Closes #264 and #769
Summary
Brings
cashu/core/errors.pyand its mint-side call sites into agreement with [cashubtc/nuts/error_codes.md] (https://github.com/cashubtc/nuts/blob/main/error_codes.md).Every change is codes-only — conditions and error messages are untouched, so callers matching on type or
detailare unaffected.