diff --git a/cashu/core/errors.py b/cashu/core/errors.py index e00453fc..eb776bbb 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -75,14 +75,14 @@ def __init__(self, detail): class SecretTooLongError(TransactionError): - code = 11003 + code = 11000 def __init__(self, detail="secret too long"): super().__init__(detail, code=self.code) class WitnessTooLongError(TransactionError): - code = 11004 + code = 11000 def __init__(self, detail="witness too long"): super().__init__(detail, code=self.code) @@ -96,13 +96,6 @@ def __init__(self): super().__init__(self.detail, code=self.code) -class TransactionUnitError(TransactionError): - code = 11009 - - def __init__(self, detail): - super().__init__(detail, code=self.code) - - class TransactionAmountExceedsLimitError(TransactionError): code = 11006 @@ -142,6 +135,30 @@ def __init__(self, detail: Optional[str] = None): super().__init__(detail, code=self.code) +class AmountlessInvoiceNotSupportedError(TransactionError): + detail = "Amountless invoice is not supported" + code = 11011 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + +class AmountMismatchError(TransactionError): + detail = "Amount in request does not equal invoice" + code = 11012 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + +class UnitNotSupportedError(TransactionError): + detail = "Unit in request is not supported" + code = 11013 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + class BatchDuplicateQuotesError(TransactionError): detail = "Duplicate quote IDs provided" code = 11016 @@ -196,16 +213,24 @@ class QuoteNotPaidError(CashuError): detail = "quote not paid" code = 20001 - def __init__(self): - super().__init__(self.detail, code=self.code) + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) class QuoteAlreadyIssuedError(CashuError): detail = "quote already issued" code = 20002 - def __init__(self): - super().__init__(self.detail, code=self.code) + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + +class MintingDisabledError(CashuError): + detail = "Minting is disabled" + code = 20003 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) class LightningPaymentFailedError(CashuError): @@ -216,6 +241,30 @@ def __init__(self, detail: Optional[str] = None): super().__init__(detail or self.detail, code=self.code) +class QuotePendingError(CashuError): + detail = "quote is pending" + code = 20005 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + +class InvoiceAlreadyPaidError(CashuError): + detail = "invoice already paid" + code = 20006 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + +class QuoteExpiredError(CashuError): + detail = "quote is expired" + code = 20007 + + def __init__(self, detail: Optional[str] = None): + super().__init__(detail or self.detail, code=self.code) + + class QuoteSignatureInvalidError(CashuError): detail = "Signature for mint request invalid" code = 20008 @@ -234,7 +283,7 @@ def __init__(self): class ClearAuthRequiredError(CashuError): detail = "Endpoint requires clear auth" - code = 80001 + code = 30001 def __init__(self): super().__init__(self.detail, code=self.code) @@ -242,7 +291,7 @@ def __init__(self): class ClearAuthFailedError(CashuError): detail = "Clear authentication failed" - code = 80002 + code = 30002 def __init__(self): super().__init__(self.detail, code=self.code) @@ -250,7 +299,7 @@ def __init__(self): class BlindAuthRequiredError(CashuError): detail = "Endpoint requires blind auth" - code = 81001 + code = 31001 def __init__(self): super().__init__(self.detail, code=self.code) @@ -258,7 +307,7 @@ def __init__(self): class BlindAuthFailedError(CashuError): detail = "Blind authentication failed" - code = 81002 + code = 31002 def __init__(self): super().__init__(self.detail, code=self.code) @@ -266,7 +315,7 @@ def __init__(self): class BlindAuthAmountExceededError(CashuError): detail = "Maximum blind auth amount exceeded" - code = 81003 + code = 31003 def __init__(self, detail: Optional[str] = None): super().__init__(detail or self.detail, code=self.code) @@ -274,7 +323,7 @@ def __init__(self, detail: Optional[str] = None): class BlindAuthRateLimitExceededError(CashuError): detail = "Blind auth token mint rate limit exceeded" - code = 81004 + code = 31004 def __init__(self): super().__init__(self.detail, code=self.code) diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index e93d30a3..f9de8c6d 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -15,7 +15,11 @@ ) from ...core.db import Connection, Database from ...core.errors import ( + InvoiceAlreadyPaidError, ProofsArePendingError, + QuoteAlreadyIssuedError, + QuoteNotPaidError, + QuotePendingError, TransactionError, ) from ..crud import LedgerCrud @@ -171,9 +175,11 @@ async def _set_mint_quote_pending(self, quote_id: str) -> MintQuote: if not quote: raise TransactionError("Mint quote not found.") if quote.pending: - raise TransactionError("Mint quote already pending.") + raise QuotePendingError("Mint quote already pending.") + if quote.issued: + raise QuoteAlreadyIssuedError(f"Mint quote {quote_id} is already issued.") if not quote.paid: - raise TransactionError("Mint quote is not paid yet.") + raise QuoteNotPaidError("Mint quote is not paid yet.") # set the quote as pending self._set_mint_quote_state(quote, MintQuoteState.pending) logger.trace(f"crud: setting quote {quote_id} as PENDING") @@ -213,11 +219,13 @@ async def _set_mint_quotes_pending(self, quote_ids: List[str]) -> List[MintQuote if not quote: raise TransactionError(f"Mint quote {quote_id} not found.") if quote.pending: - raise TransactionError(f"Mint quote {quote_id} already pending.") + raise QuotePendingError(f"Mint quote {quote_id} already pending.") if quote.issued: - raise TransactionError(f"Mint quote {quote_id} is already issued.") + raise QuoteAlreadyIssuedError( + f"Mint quote {quote_id} is already issued." + ) if not quote.paid: - raise TransactionError(f"Mint quote {quote_id} is not paid yet.") + raise QuoteNotPaidError(f"Mint quote {quote_id} is not paid yet.") # set the quote as pending self._set_mint_quote_state(quote, MintQuoteState.pending) @@ -330,13 +338,10 @@ async def _set_melt_quote_pending( ) if len(quotes_db) == 0: raise TransactionError("Melt quote not found.") - if any( - [ - quote.state in [MeltQuoteState.pending, MeltQuoteState.paid] - for quote in quotes_db - ] - ): - raise TransactionError("Melt quote already paid or pending.") + if any([quote.state == MeltQuoteState.paid for quote in quotes_db]): + raise InvoiceAlreadyPaidError("Melt quote already paid or pending.") + if any([quote.state == MeltQuoteState.pending for quote in quotes_db]): + raise QuotePendingError("Melt quote already paid or pending.") # set the quote as pending quote_copy.state = MeltQuoteState.pending await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn) @@ -441,13 +446,10 @@ async def _store_melt_quote(self, quote: MeltQuote): quotes_db = await self.crud.get_melt_quotes_by_checking_id( checking_id=quote.checking_id, db=self.db, conn=conn ) - if any( - [ - quote.state in [MeltQuoteState.pending, MeltQuoteState.paid] - for quote in quotes_db - ] - ): - raise TransactionError("Melt quote already paid or pending.") + if any([quote.state == MeltQuoteState.paid for quote in quotes_db]): + raise InvoiceAlreadyPaidError("Melt quote already paid or pending.") + if any([quote.state == MeltQuoteState.pending for quote in quotes_db]): + raise QuotePendingError("Melt quote already paid or pending.") # store the melt quote await self.crud.store_melt_quote(quote=quote, db=self.db, conn=conn) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 070ad8de..e4926739 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -28,14 +28,20 @@ from ..core.crypto.secp import PrivateKey, PublicKey from ..core.db import Connection, Database from ..core.errors import ( + AmountlessInvoiceNotSupportedError, + AmountMismatchError, BatchDuplicateQuotesError, CashuError, + InvoiceAlreadyPaidError, KeysetInactiveError, LightningError, LightningPaymentFailedError, + MintingDisabledError, NotAllowedError, QuoteAlreadyIssuedError, + QuoteExpiredError, QuoteNotPaidError, + QuotePendingError, QuoteSignatureInvalidError, TransactionAmountExceedsLimitError, TransactionError, @@ -329,7 +335,7 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote: f"Maximum mint amount is {settings.mint_max_mint_bolt11_sat} sat." ) if settings.mint_bolt11_disable_mint: - raise NotAllowedError("Minting with bolt11 is disabled.") + raise MintingDisabledError("Minting with bolt11 is disabled.") unit, method = self._verify_and_get_unit_method( quote_request.unit, Method.bolt11.name @@ -510,7 +516,7 @@ async def mint( quote = await self.get_mint_quote(quote_id) if quote.pending: - raise TransactionError("Mint quote already pending.") + raise QuotePendingError("Mint quote already pending.") if quote.issued: raise QuoteAlreadyIssuedError() if quote.state != MintQuoteState.paid: @@ -524,7 +530,7 @@ async def mint( if not quote.amount == sum_amount_outputs: raise TransactionError("amount to mint does not match quote amount") if quote.expiry and quote.expiry < int(time.time()): - raise TransactionError("quote expired") + raise QuoteExpiredError("quote expired") if not self._verify_mint_quote_witness(quote, outputs, signature): raise QuoteSignatureInvalidError() await self._store_blinded_messages(outputs, mint_id=quote_id) @@ -595,7 +601,7 @@ async def mint_batch( for quote in quotes: if quote.pending: - raise TransactionError("mint quote already pending") + raise QuotePendingError("mint quote already pending") if quote.issued: raise QuoteAlreadyIssuedError() if quote.state != MintQuoteState.paid: @@ -646,7 +652,7 @@ async def mint_batch( try: for quote in quotes: if quote.expiry and quote.expiry < int(time.time()): - raise TransactionError("quote expired") + raise QuoteExpiredError("quote expired") # Store all blinded messages await self._store_blinded_messages( @@ -685,9 +691,9 @@ def create_internal_melt_quote( if not mint_quote.method == method.name: raise TransactionError("methods do not match") if mint_quote.paid: - raise TransactionError("mint quote already paid") + raise InvoiceAlreadyPaidError("mint quote already paid") if mint_quote.issued: - raise TransactionError("mint quote already issued") + raise QuoteAlreadyIssuedError("mint quote already issued") if not mint_quote.unpaid: raise TransactionError("mint quote is not unpaid") @@ -728,7 +734,7 @@ def validate_payment_quote( logger.error( f"expected {payment_quote.amount.to(Unit.msat).amount} msat but got {melt_quote.mpp_amount}" ) - raise TransactionError("quote amount not as requested") + raise AmountMismatchError("quote amount not as requested") # make sure the backend returned the amount with a correct unit if not payment_quote.amount.unit == unit: raise TransactionError("payment quote amount units do not match") @@ -798,7 +804,7 @@ async def melt_quote( # support only the bol11 method for now. invoice_obj = bolt11.decode(melt_quote.request) if not invoice_obj.amount_msat: - raise TransactionError("invoice has no amount.") + raise AmountlessInvoiceNotSupportedError("invoice has no amount.") # we set the expiry of this quote to the expiry of the bolt11 invoice now = int(time.time()) expiry = None @@ -979,25 +985,25 @@ async def melt_mint_settle_internally( # we settle the transaction internally if melt_quote.state == MeltQuoteState.paid: - raise TransactionError("melt quote already paid") + raise InvoiceAlreadyPaidError("melt quote already paid") # verify amounts from bolt11 invoice bolt11_request = melt_quote.request invoice_obj = bolt11.decode(bolt11_request) if not invoice_obj.amount_msat: - raise TransactionError("invoice has no amount.") + raise AmountlessInvoiceNotSupportedError("invoice has no amount.") if not mint_quote.amount == melt_quote.amount: - raise TransactionError("amounts do not match") + raise AmountMismatchError("amounts do not match") if not bolt11_request == mint_quote.request: raise TransactionError("bolt11 requests do not match") if not mint_quote.method == melt_quote.method: raise TransactionError("methods do not match") if mint_quote.paid: - raise TransactionError("mint quote already paid") + raise InvoiceAlreadyPaidError("mint quote already paid") if mint_quote.issued: - raise TransactionError("mint quote already issued") + raise QuoteAlreadyIssuedError("mint quote already issued") if mint_quote.state != MintQuoteState.unpaid: raise TransactionError("mint quote is not unpaid") @@ -1096,8 +1102,12 @@ async def _prepare_melt( # get melt quote and check if it was already paid melt_quote = await self.get_melt_quote(quote_id=quote) - if not melt_quote.unpaid: - raise TransactionError(f"melt quote is not unpaid: {melt_quote.state}") + if melt_quote.paid: + raise InvoiceAlreadyPaidError( + f"melt quote is not unpaid: {melt_quote.state}" + ) + if melt_quote.pending: + raise QuotePendingError(f"melt quote is not unpaid: {melt_quote.state}") unit, _ = self._verify_and_get_unit_method(melt_quote.unit, melt_quote.method) diff --git a/cashu/mint/verification.py b/cashu/mint/verification.py index 7551178a..594d4bcb 100644 --- a/cashu/mint/verification.py +++ b/cashu/mint/verification.py @@ -25,8 +25,8 @@ TransactionDuplicateOutputsError, TransactionError, TransactionMultipleUnitsError, - TransactionUnitError, TransactionUnitMismatchError, + UnitNotSupportedError, WitnessTooLongError, ) from ..core.nuts import nut20 @@ -351,7 +351,7 @@ def _verify_units_match( def get_fees_for_proofs(self, proofs: List[Proof]) -> int: if not len({self.keysets[p.id].unit for p in proofs}) == 1: - raise TransactionUnitError("inputs have different units.") + raise TransactionMultipleUnitsError("inputs have different units.") fee = (sum([self.keysets[p.id].input_fee_ppk for p in proofs]) + 999) // 1000 return fee @@ -385,7 +385,9 @@ def _verify_and_get_unit_method( unit = Unit[unit_str] if not any([unit == k.unit for k in self.keysets.values()]): - raise NotAllowedError(f"unit '{unit.name}' not supported in any keyset.") + raise UnitNotSupportedError( + f"unit '{unit.name}' not supported in any keyset." + ) if not self.backends.get(method) or unit not in self.backends[method]: raise NotAllowedError( diff --git a/tests/mint/test_mint_auth_server_unit.py b/tests/mint/test_mint_auth_server_unit.py index 9644d307..8e6be7b9 100644 --- a/tests/mint/test_mint_auth_server_unit.py +++ b/tests/mint/test_mint_auth_server_unit.py @@ -9,7 +9,9 @@ BlindAuthAmountExceededError, BlindAuthFailedError, BlindAuthRateLimitExceededError, + BlindAuthRequiredError, ClearAuthFailedError, + ClearAuthRequiredError, ) from cashu.core.secret import Secret, Tags from cashu.core.settings import settings @@ -28,6 +30,25 @@ def _auth_token(secret: str = "secret", proof_id: str = "kid") -> str: return AuthProof.from_proof(proof).to_base64() +@pytest.mark.parametrize( + "error_class, code", + [ + (ClearAuthRequiredError, 30001), + (ClearAuthFailedError, 30002), + (BlindAuthRequiredError, 31001), + (BlindAuthFailedError, 31002), + (BlindAuthAmountExceededError, 31003), + (BlindAuthRateLimitExceededError, 31004), + ], +) +def test_auth_error_codes_match_nut21_and_nut22(error_class, code): + """Auth error codes are specified in NUT-21 and NUT-22, see + https://github.com/cashubtc/nuts/blob/main/error_codes.md + """ + assert error_class.code == code + assert error_class().code == code + + def test_verify_oicd_issuer_accepts_matching_issuer(): ledger = _ledger() ledger.issuer = "https://issuer.test" @@ -98,8 +119,9 @@ async def get_user(decoded): monkeypatch.setattr(ledger, "_get_user", get_user) - with pytest.raises(ClearAuthFailedError): + with pytest.raises(ClearAuthFailedError) as exc_info: await ledger.verify_clear_auth("token") + assert exc_info.value.code == 30002 @pytest.mark.asyncio @@ -118,8 +140,9 @@ def fail_limit(identifier, *args, **kwargs): monkeypatch.setattr("cashu.mint.auth.server.assert_limit", fail_limit) - with pytest.raises(BlindAuthRateLimitExceededError): + with pytest.raises(BlindAuthRateLimitExceededError) as exc_info: await ledger.verify_clear_auth("token") + assert exc_info.value.code == 31004 @pytest.mark.asyncio @@ -147,8 +170,9 @@ async def test_mint_blind_auth_enforces_maximum_outputs(monkeypatch): monkeypatch.setattr(settings, "mint_auth_max_blind_tokens", 2) outputs = [BlindedMessage(id="kid", amount=1, B_=f"b{i}") for i in range(3)] - with pytest.raises(BlindAuthAmountExceededError, match="Too many outputs"): + with pytest.raises(BlindAuthAmountExceededError, match="Too many outputs") as exc_info: await ledger.mint_blind_auth(outputs=outputs, user=User(id="alice")) + assert exc_info.value.code == 31003 @pytest.mark.asyncio @@ -264,9 +288,10 @@ async def test_verify_blind_auth_rejects_malformed_nut10_secret(): ).serialize() ) - with pytest.raises(BlindAuthFailedError): + with pytest.raises(BlindAuthFailedError) as exc_info: async with ledger.verify_blind_auth(token): pass + assert exc_info.value.code == 31002 @pytest.mark.asyncio diff --git a/tests/mint/test_mint_db.py b/tests/mint/test_mint_db.py index cdfe617d..cb4de1aa 100644 --- a/tests/mint/test_mint_db.py +++ b/tests/mint/test_mint_db.py @@ -6,7 +6,7 @@ from fastapi import WebSocket from cashu.core.base import MeltQuoteState, MintQuoteState -from cashu.core.errors import ProofsArePendingError +from cashu.core.errors import ProofsArePendingError, QuoteAlreadyIssuedError from cashu.core.json_rpc.base import ( JSONRPCMethods, JSONRPCNotficationParams, @@ -208,6 +208,26 @@ async def set_state(quote, state): ) +@pytest.mark.asyncio +async def test_mint_quote_set_pending_rejects_issued_quote( + wallet: Wallet, ledger: Ledger +): + """An issued quote is neither pending nor paid, so the locked guard must not + fall through to QuoteNotPaidError.""" + mint_quote = await wallet.request_mint(128) + await pay_if_regtest(mint_quote.request) + _ = await ledger.get_mint_quote(mint_quote.quote) + + quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db) + assert quote is not None + quote.state = MintQuoteState.issued + await ledger.crud.update_mint_quote(quote=quote, db=ledger.db) + + with pytest.raises(QuoteAlreadyIssuedError) as exc_info: + await ledger.db_write._set_mint_quote_pending(quote.quote) + assert exc_info.value.code == 20002 + + @pytest.mark.asyncio async def test_mint_quote_set_pending(wallet: Wallet, ledger: Ledger): mint_quote = await wallet.request_mint(128) diff --git a/tests/mint/test_mint_melt.py b/tests/mint/test_mint_melt.py index 8d0e435d..ca7964b7 100644 --- a/tests/mint/test_mint_melt.py +++ b/tests/mint/test_mint_melt.py @@ -13,9 +13,11 @@ Unit, ) from cashu.core.errors import ( + InvoiceAlreadyPaidError, LightningPaymentFailedError, OutputsAlreadySignedError, OutputsArePendingError, + QuotePendingError, ) from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest from cashu.core.settings import settings @@ -593,8 +595,6 @@ async def test_set_melt_quote_pending_without_checking_id(ledger: Ledger): @pytest.mark.asyncio async def test_set_melt_quote_pending_prevents_duplicate_checking_id(ledger: Ledger): """Test that setting a melt quote as pending fails if another quote with same checking_id is already pending.""" - from cashu.core.errors import TransactionError - checking_id = "test_checking_id_duplicate" quote1 = MeltQuote( @@ -633,9 +633,10 @@ async def test_set_melt_quote_pending_prevents_duplicate_checking_id(ledger: Led # Attempt to set the second quote as pending should fail try: await ledger.db_write._set_melt_quote_pending(quote=quote2) - raise AssertionError("Expected TransactionError") - except TransactionError as e: + raise AssertionError("Expected QuotePendingError") + except QuotePendingError as e: assert "Melt quote already paid or pending." in str(e) + assert e.code == 20005 # Verify the second quote is still unpaid quote2_db = await ledger.crud.get_melt_quote( @@ -987,3 +988,24 @@ async def patched_pay_invoice(quote: MeltQuote, fee_limit_msat: int): f"Expected no orphan blank outputs for melt {melt_quote.quote}, " f"got {len(orphans)} with B_s {[o.B_ for o in orphans]}" ) + + +@pytest.mark.asyncio +async def test_prepare_melt_rejects_already_paid_quote(ledger: Ledger): + """_prepare_melt runs before the locked guard, so it must report the paid + state with its own code rather than a generic transaction error.""" + quote = MeltQuote( + quote="quote_id_already_paid", + method="bolt11", + request="lnbcfake", + checking_id="checking_id_already_paid", + unit="sat", + state=MeltQuoteState.paid, + amount=100, + fee_reserve=1, + ) + await ledger.crud.store_melt_quote(quote=quote, db=ledger.db) + + with pytest.raises(InvoiceAlreadyPaidError) as exc_info: + await ledger._prepare_melt(proofs=[], quote=quote.quote) + assert exc_info.value.code == 20006 diff --git a/tests/mint/test_mint_operations.py b/tests/mint/test_mint_operations.py index d13fb523..f76aab5d 100644 --- a/tests/mint/test_mint_operations.py +++ b/tests/mint/test_mint_operations.py @@ -2,7 +2,14 @@ import pytest_asyncio from cashu.core.base import MeltQuoteState, MintQuoteState -from cashu.core.errors import OutputsAlreadySignedError, ProofsAlreadySpentError +from cashu.core.errors import ( + InvoiceAlreadyPaidError, + MintingDisabledError, + OutputsAlreadySignedError, + ProofsAlreadySpentError, + QuoteExpiredError, + QuotePendingError, +) from cashu.core.helpers import sum_proofs from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest from cashu.core.nuts import nut20 @@ -530,6 +537,54 @@ async def test_melt_preserves_change_signatures_order_integration(wallet1: Walle for i, proof in enumerate(change_proofs): assert proof.amount == expected_amounts[i] + +@pytest.mark.parametrize( + "error_class, code", + [ + (MintingDisabledError, 20003), + (QuotePendingError, 20005), + (InvoiceAlreadyPaidError, 20006), + (QuoteExpiredError, 20007), + ], +) +def test_quote_lifecycle_error_codes(error_class, code): + assert error_class.code == code + assert error_class().code == code + + +@pytest.mark.asyncio +async def test_mint_quote_disabled_raises_minting_disabled(ledger: Ledger, monkeypatch): + monkeypatch.setattr(settings, "mint_bolt11_disable_mint", True) + + with pytest.raises(MintingDisabledError) as exc_info: + await ledger.mint_quote(PostMintQuoteRequest(unit="sat", amount=128)) + assert exc_info.value.code == 20003 + + +@pytest.mark.asyncio +@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") +async def test_mint_pending_quote_raises_quote_pending( + wallet1: Wallet, ledger: Ledger +): + wallet_mint_quote = await wallet1.request_mint(128) + mint_quote = await ledger.get_mint_quote(wallet_mint_quote.quote) + assert mint_quote.state == MintQuoteState.paid + + secrets, rs, _ = await wallet1.generate_n_secrets(1) + outputs, rs = wallet1._construct_outputs([128], secrets, rs) + + await ledger.db_write._set_mint_quote_pending(mint_quote.quote) + try: + with pytest.raises(QuotePendingError) as exc_info: + await ledger.mint(outputs=outputs, quote_id=mint_quote.quote) + assert exc_info.value.code == 20005 + finally: + await ledger.db_write._unset_mint_quote_pending( + mint_quote.quote, MintQuoteState.paid + ) + + + # TODO: test keeps running forever, needs to be fixed # @pytest.mark.asyncio # async def test_websocket_quote_updates(wallet1: Wallet, ledger: Ledger): diff --git a/tests/mint/test_mint_verification.py b/tests/mint/test_mint_verification.py index 8e5a8482..35f15ef2 100644 --- a/tests/mint/test_mint_verification.py +++ b/tests/mint/test_mint_verification.py @@ -17,6 +17,8 @@ from cashu.core.crypto.b_dhke import hash_to_curve, step1_alice from cashu.core.crypto.secp import PrivateKey from cashu.core.errors import ( + AmountlessInvoiceNotSupportedError, + AmountMismatchError, InvalidProofsError, KeysetInactiveError, NoSecretInProofsError, @@ -29,8 +31,8 @@ TransactionDuplicateOutputsError, TransactionError, TransactionMultipleUnitsError, - TransactionUnitError, TransactionUnitMismatchError, + UnitNotSupportedError, WitnessTooLongError, ) from cashu.core.nuts import nut11, nut20 @@ -101,8 +103,9 @@ def test_verify_secret_criteria_rejects_too_long_secret(ledger: Ledger): secret="x" * (settings.mint_max_secret_length + 1), C="02" + "ab" * 32, ) - with pytest.raises(SecretTooLongError): + with pytest.raises(SecretTooLongError) as exc_info: ledger._verify_secret_criteria(p) + assert exc_info.value.code == 11000 # --------------------------------------------------------------------------- @@ -147,8 +150,9 @@ def test_verify_input_witness_criteria_rejects_long_witness(ledger: Ledger): C="02" + "ab" * 32, witness="w" * (settings.mint_max_witness_length + 1), ) - with pytest.raises(WitnessTooLongError): + with pytest.raises(WitnessTooLongError) as exc_info: ledger._verify_input_witness_criteria(p) + assert exc_info.value.code == 11000 # --------------------------------------------------------------------------- @@ -282,8 +286,11 @@ def test_get_fees_for_proofs_rejects_mixed_units(ledger: Ledger): p1.id = "k_sat" p2 = MagicMock() p2.id = "k_usd" - with pytest.raises(TransactionUnitError, match="inputs have different units"): + with pytest.raises( + TransactionMultipleUnitsError, match="inputs have different units" + ) as exc_info: ledger.get_fees_for_proofs([p1, p2]) + assert exc_info.value.code == 11009 finally: ledger.keysets = orig @@ -331,6 +338,19 @@ def test_verify_equation_balanced_rejects_unbalanced(ledger: Ledger): # --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "error_class, code", + [ + (AmountlessInvoiceNotSupportedError, 11011), + (AmountMismatchError, 11012), + (UnitNotSupportedError, 11013), + ], +) +def test_transaction_error_codes(error_class, code): + assert error_class.code == code + assert error_class().code == code + + def test_verify_and_get_unit_method_accepts_bolt11_sat(ledger: Ledger): u, m = ledger._verify_and_get_unit_method("sat", "bolt11") assert u == Unit.sat @@ -338,8 +358,11 @@ def test_verify_and_get_unit_method_accepts_bolt11_sat(ledger: Ledger): def test_verify_and_get_unit_method_rejects_unknown_unit(ledger: Ledger): - with pytest.raises(NotAllowedError, match="not supported in any keyset"): + with pytest.raises( + UnitNotSupportedError, match="not supported in any keyset" + ) as exc_info: ledger._verify_and_get_unit_method("auth", "bolt11") + assert exc_info.value.code == 11013 def test_verify_and_get_unit_method_rejects_unsupported_backend(ledger: Ledger):