diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cedb438e..3408f535 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,6 @@ jobs: os: [ubuntu-latest] python-version: ["3.12"] poetry-version: ["2.3.2"] - # Wallet v0 / deprecated mint API support was removed in https://github.com/cashubtc/nutshell/pull/814; deprecated-only CI runs are no longer valid. - # mint-only-deprecated: ["false", "true"] - mint-only-deprecated: ["false"] mint-database: ["./test_data/test_mint", "postgres://cashu:cashu@localhost:5432/cashu"] backend-wallet-class: ["FakeWallet"] uses: ./.github/workflows/tests.yml @@ -29,7 +26,6 @@ jobs: os: ${{ matrix.os }} python-version: ${{ matrix.python-version }} poetry-version: ${{ matrix.poetry-version }} - mint-only-deprecated: ${{ matrix.mint-only-deprecated }} mint-database: ${{ matrix.mint-database }} tests_redis_cache: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86c64cec..ab16be2d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,16 +15,13 @@ on: os: default: "ubuntu-latest" type: string - mint-only-deprecated: - default: "false" - type: string permissions: contents: read jobs: poetry: - name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }}) + name: Run (db ${{ inputs.mint-database }}) runs-on: ${{ inputs.os }} steps: - name: Start PostgreSQL service @@ -45,7 +42,6 @@ jobs: MINT_HOST: localhost MINT_PORT: 3337 MINT_TEST_DATABASE: ${{ inputs.mint-database }} - DEBUG_MINT_ONLY_DEPRECATED: ${{ inputs.mint-only-deprecated }} TOR: false run: | make test diff --git a/.github/workflows/tests_redis_cache.yml b/.github/workflows/tests_redis_cache.yml index 5816163f..f4616ade 100644 --- a/.github/workflows/tests_redis_cache.yml +++ b/.github/workflows/tests_redis_cache.yml @@ -15,16 +15,13 @@ on: os: default: "ubuntu-latest" type: string - mint-only-deprecated: - default: "false" - type: string permissions: contents: read jobs: poetry: - name: Run (db ${{ inputs.mint-database }}, deprecated api ${{ inputs.mint-only-deprecated }}) + name: Run (db ${{ inputs.mint-database }}) runs-on: ${{ inputs.os }} steps: - name: Start PostgreSQL service diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1220c6dc..b9920f24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,3 @@ MINT_CORELIGHTNING_REST_CERT=../cashu-regtest-enviroment/data/clightning-2-rest/ ### Profiling If you'd like to profile your code (measure how long steps take to execute), run the mint using `DEBUG_PROFILING=TRUE`. Make sure to turn this off again, as your application will be significantly slower with profiling enabled. - -### V0 API only - -To run the mint with only V0 API support (deprecated), use `DEBUG_MINT_ONLY_DEPRECATED=TRUE` diff --git a/cashu/core/base.py b/cashu/core/base.py index aedd9dbd..de3538b9 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -241,23 +241,6 @@ def from_row(cls, row: RowMapping): return cls(amount=row["amount"], B_=row["b_"], id=row["id"], C_=row.get("c_")) -class BlindedMessage_Deprecated(BaseModel): - """ - Deprecated: BlindedMessage for v0 protocol (deprecated api routes) have no id field. - - Blinded message or blinded secret or "output" which is to be signed by the mint - """ - - amount: int - B_: str # Hex-encoded blinded message - witness: Union[str, None] = None # witnesses (used for P2PK with SIG_ALL) - - @property - def p2pksigs(self) -> List[str]: - assert self.witness, "Witness missing in output" - return P2PKWitness.from_witness(self.witness).signatures - - class BlindedSignature(BaseModel): """ Blinded signature or "promise" which is the signature on a `BlindedMessage` @@ -303,7 +286,6 @@ class MeltQuote(LedgerEvent): fee_paid: int = 0 payment_preimage: Optional[str] = None expiry: Optional[int] = None - outputs: Optional[List[BlindedMessage]] = None change: Optional[List[BlindedSignature]] = None mint: Optional[str] = None @@ -322,10 +304,6 @@ def from_row(cls, row: Row, change: Optional[List[BlindedSignature]] = None): payment_preimage = row.get("payment_preimage") or row.get("proof") # type: ignore - outputs = None - if "outputs" in row.keys() and row["outputs"]: - outputs = json.loads(row["outputs"]) - return cls( quote=row["quote"], method=row["method"], @@ -338,22 +316,19 @@ def from_row(cls, row: Row, change: Optional[List[BlindedSignature]] = None): created_time=created_time, paid_time=paid_time, fee_paid=row["fee_paid"], - outputs=outputs, change=change, expiry=expiry, payment_preimage=payment_preimage, ) @classmethod - def from_resp_wallet(cls, melt_quote_resp, mint: str, unit: str, request: str): + def from_resp_wallet(cls, melt_quote_resp, mint: str): return cls( quote=melt_quote_resp.quote, method=Method.bolt11.name, - request=melt_quote_resp.request - or request, # BACKWARDS COMPATIBILITY mint response < 0.17.0 + request=melt_quote_resp.request, checking_id="", - unit=melt_quote_resp.unit - or unit, # BACKWARDS COMPATIBILITY mint response < 0.17.0 + unit=melt_quote_resp.unit, amount=melt_quote_resp.amount, fee_reserve=melt_quote_resp.fee_reserve, state=MeltQuoteState(melt_quote_resp.state), @@ -452,11 +427,7 @@ def from_row(cls, row: Row): if "last_checked" in row.keys() and row["last_checked"] else None ) - updated_at = ( - int(row["updated_at"]) - if "updated_at" in row.keys() and row["updated_at"] - else None - ) + updated_at = int(row["updated_at"]) if row["updated_at"] else None except Exception: # POSTGRES: row is datetime.datetime created_time = ( @@ -474,9 +445,7 @@ def from_row(cls, row: Row): else None ) updated_at = ( - int(row["updated_at"].timestamp()) - if "updated_at" in row.keys() and row["updated_at"] - else None + int(row["updated_at"].timestamp()) if row["updated_at"] else None ) return cls( quote=row["quote"], @@ -492,12 +461,10 @@ def from_row(cls, row: Row): last_checked=last_checked, pubkey=row["pubkey"] if "pubkey" in row.keys() else None, privkey=row["privkey"] if "privkey" in row.keys() else None, - amount_paid=row["amount_paid"] - if "amount_paid" in row.keys() and row["amount_paid"] is not None - else None, - amount_issued=row["amount_issued"] - if "amount_issued" in row.keys() and row["amount_issued"] is not None - else None, + amount_paid=row["amount_paid"] if row["amount_paid"] is not None else None, + amount_issued=( + row["amount_issued"] if row["amount_issued"] is not None else None + ), updated_at=updated_at if updated_at is not None else (issued_time or paid_time or created_time or int(time.time())), @@ -508,8 +475,6 @@ def from_resp_wallet( cls, mint_quote_resp, mint: str, - amount: int, - unit: str, paid_time: Optional[int] = None, issued_time: Optional[int] = None, ): @@ -532,10 +497,8 @@ def from_resp_wallet( state = MintQuoteState.issued else: state = MintQuoteState.unpaid - elif mint_quote_resp.state: - state = MintQuoteState(mint_quote_resp.state) else: - state = MintQuoteState.unpaid + state = MintQuoteState(mint_quote_resp.state) if paid_time is None and mint_quote_resp.updated_at is not None: if state in [MintQuoteState.paid, MintQuoteState.issued]: @@ -550,10 +513,8 @@ def from_resp_wallet( method=Method.bolt11.name, request=mint_quote_resp.request, checking_id="", - unit=mint_quote_resp.unit - or unit, # BACKWARDS COMPATIBILITY mint response < 0.17.0 - amount=mint_quote_resp.amount - or amount, # BACKWARDS COMPATIBILITY mint response < 0.17.0 + unit=mint_quote_resp.unit, + amount=mint_quote_resp.amount, state=state, mint=mint, expiry=mint_quote_resp.expiry, @@ -572,8 +533,6 @@ def check_stale_and_from_resp_wallet( mint_quote_resp, mint: str, mint_quote_local: Optional["MintQuote"] = None, - default_amount: int = 0, - default_unit: str = "sat", ) -> "MintQuote": # Check if the response from the mint is stale compared to the local quote is_stale = False @@ -607,25 +566,12 @@ def check_stale_and_from_resp_wallet( if is_stale and mint_quote_local: return mint_quote_local - amount = ( - (mint_quote_resp.amount or mint_quote_local.amount) - if mint_quote_local - else default_amount - ) - unit = ( - (mint_quote_resp.unit or mint_quote_local.unit) - if mint_quote_local - else default_unit - ) - paid_time = mint_quote_local.paid_time if mint_quote_local else None issued_time = mint_quote_local.issued_time if mint_quote_local else None return cls.from_resp_wallet( mint_quote_resp, mint=mint, - amount=amount, - unit=unit, paid_time=paid_time, issued_time=issued_time, ) @@ -992,8 +938,6 @@ class MintKeyset: balance: int final_expiry: Optional[int] = None # NEW: Final expiry timestamp for keyset v2 - duplicate_keyset_id: Optional[str] = None # BACKWARDS COMPATIBILITY < 0.15.0 - def __init__( self, *, diff --git a/cashu/core/models/__init__.py b/cashu/core/models/__init__.py index 841d5825..fdcacc46 100644 --- a/cashu/core/models/__init__.py +++ b/cashu/core/models/__init__.py @@ -1,15 +1,7 @@ from .blind_auth import PostAuthBlindMintRequest, PostAuthBlindMintResponse -from .check import ( - CheckFeesRequest_deprecated, - CheckFeesResponse_deprecated, - CheckSpendableRequest_deprecated, - CheckSpendableResponse_deprecated, - PostCheckStateRequest, - PostCheckStateResponse, -) +from .check import PostCheckStateRequest, PostCheckStateResponse from .info import ( GetInfoResponse, - GetInfoResponse_deprecated, MeltMethodSetting, MintInfoContact, MintInfoProtectedEndpoint, @@ -19,17 +11,11 @@ ) from .keys import ( KeysetsResponse, - KeysetsResponse_deprecated, KeysetsResponseKeyset, KeysResponse, - KeysResponse_deprecated, KeysResponseKeyset, ) -from .melt import ( - PostMeltRequest, - PostMeltRequest_deprecated, - PostMeltResponse_deprecated, -) +from .melt import PostMeltRequest from .melt_quote import ( PostMeltQuoteRequest, PostMeltQuoteResponse, @@ -37,43 +23,25 @@ PostMeltRequestOptions, ) from .mint import ( - GetMintResponse_deprecated, PostMintBatchRequest, PostMintBatchResponse, PostMintRequest, - PostMintRequest_deprecated, PostMintResponse, - PostMintResponse_deprecated, ) from .mint_quote import ( PostMintQuoteCheckRequest, PostMintQuoteRequest, PostMintQuoteResponse, ) -from .restore import ( - PostRestoreRequest, - PostRestoreRequest_Deprecated, - PostRestoreResponse, -) -from .swap import ( - PostSwapRequest, - PostSwapRequest_Deprecated, - PostSwapResponse, - PostSwapResponse_Deprecated, - PostSwapResponse_Very_Deprecated, -) +from .restore import PostRestoreRequest, PostRestoreResponse +from .swap import PostSwapRequest, PostSwapResponse __all__ = [ "PostAuthBlindMintRequest", "PostAuthBlindMintResponse", - "CheckFeesRequest_deprecated", - "CheckFeesResponse_deprecated", - "CheckSpendableRequest_deprecated", - "CheckSpendableResponse_deprecated", "PostCheckStateRequest", "PostCheckStateResponse", "GetInfoResponse", - "GetInfoResponse_deprecated", "MeltMethodSetting", "MintInfoContact", "MintInfoProtectedEndpoint", @@ -81,34 +49,23 @@ "MintMethodSetting", "Nut15MppSupport", "KeysResponse", - "KeysResponse_deprecated", "KeysResponseKeyset", "KeysetsResponse", - "KeysetsResponse_deprecated", "KeysetsResponseKeyset", "PostMeltRequest", - "PostMeltRequest_deprecated", - "PostMeltResponse_deprecated", "PostMeltQuoteRequest", "PostMeltQuoteResponse", "PostMeltRequestOptionMpp", "PostMeltRequestOptions", - "GetMintResponse_deprecated", "PostMintBatchRequest", "PostMintBatchResponse", "PostMintRequest", - "PostMintRequest_deprecated", "PostMintResponse", - "PostMintResponse_deprecated", "PostMintQuoteCheckRequest", "PostMintQuoteRequest", "PostMintQuoteResponse", "PostRestoreRequest", - "PostRestoreRequest_Deprecated", "PostRestoreResponse", "PostSwapRequest", - "PostSwapRequest_Deprecated", "PostSwapResponse", - "PostSwapResponse_Deprecated", - "PostSwapResponse_Very_Deprecated", ] diff --git a/cashu/core/models/check.py b/cashu/core/models/check.py index 68d3fb7a..33614e3a 100644 --- a/cashu/core/models/check.py +++ b/cashu/core/models/check.py @@ -1,9 +1,9 @@ -from typing import Annotated, List, Union +from typing import Annotated, List from pydantic import BaseModel, Field -from cashu.core.base import Proof, ProofState -from cashu.core.constants import MAX_PAYMENT_REQUEST_LEN, MAX_PUBKEY_LEN +from cashu.core.base import ProofState +from cashu.core.constants import MAX_PUBKEY_LEN from cashu.core.settings import settings @@ -15,20 +15,3 @@ class PostCheckStateRequest(BaseModel): class PostCheckStateResponse(BaseModel): states: List[ProofState] = [] - - -class CheckSpendableRequest_deprecated(BaseModel): - proofs: List[Proof] = Field(..., max_length=settings.mint_max_request_length) - - -class CheckSpendableResponse_deprecated(BaseModel): - spendable: List[bool] - pending: List[bool] - - -class CheckFeesRequest_deprecated(BaseModel): - pr: str = Field(..., max_length=MAX_PAYMENT_REQUEST_LEN) - - -class CheckFeesResponse_deprecated(BaseModel): - fee: Union[int, None] diff --git a/cashu/core/models/info.py b/cashu/core/models/info.py index 7c48f5c4..5211038a 100644 --- a/cashu/core/models/info.py +++ b/cashu/core/models/info.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, model_validator +from pydantic import BaseModel class MintMethodBolt11OptionSetting(BaseModel): @@ -51,35 +51,7 @@ class GetInfoResponse(BaseModel): def supports(self, nut: int) -> Optional[bool]: return nut in self.nuts if self.nuts else None - # BEGIN DEPRECATED: NUT-06 contact field change - # NUT-06 PR: https://github.com/cashubtc/nuts/pull/117 - @model_validator(mode="before") - @classmethod - def preprocess_deprecated_contact_field(cls, values: dict): - if "contact" in values and values["contact"]: - if isinstance(values["contact"][0], list): - values["contact"] = [ - MintInfoContact(method=method, info=info) - for method, info in values["contact"] - if method and info - ] - return values - - # END DEPRECATED: NUT-06 contact field change - class Nut15MppSupport(BaseModel): method: str unit: str - - -class GetInfoResponse_deprecated(BaseModel): - name: Optional[str] = None - pubkey: Optional[str] = None - version: Optional[str] = None - description: Optional[str] = None - description_long: Optional[str] = None - contact: Optional[List[List[str]]] = None - nuts: Optional[List[str]] = None - motd: Optional[str] = None - parameter: Optional[dict] = None diff --git a/cashu/core/models/keys.py b/cashu/core/models/keys.py index c4b6d4d1..c8a012d4 100644 --- a/cashu/core/models/keys.py +++ b/cashu/core/models/keys.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from pydantic import BaseModel, RootModel +from pydantic import BaseModel class KeysResponseKeyset(BaseModel): @@ -26,11 +26,3 @@ class KeysetsResponseKeyset(BaseModel): class KeysetsResponse(BaseModel): keysets: list[KeysetsResponseKeyset] - - -class KeysResponse_deprecated(RootModel): - root: Dict[str, str] - - -class KeysetsResponse_deprecated(BaseModel): - keysets: list[str] diff --git a/cashu/core/models/melt.py b/cashu/core/models/melt.py index be553969..e0fc2aa6 100644 --- a/cashu/core/models/melt.py +++ b/cashu/core/models/melt.py @@ -4,11 +4,9 @@ from cashu.core.base import ( BlindedMessage, - BlindedMessage_Deprecated, - BlindedSignature, Proof, ) -from cashu.core.constants import MAX_PAYMENT_REQUEST_LEN, MAX_QUOTE_ID_LEN +from cashu.core.constants import MAX_QUOTE_ID_LEN from cashu.core.settings import settings @@ -19,17 +17,3 @@ class PostMeltRequest(BaseModel): None, max_length=settings.mint_max_request_length ) prefer_async: Optional[bool] = None - - -class PostMeltResponse_deprecated(BaseModel): - paid: Union[bool, None] - preimage: Union[str, None] - change: Union[List[BlindedSignature], None] = None - - -class PostMeltRequest_deprecated(BaseModel): - proofs: List[Proof] = Field(..., max_length=settings.mint_max_request_length) - pr: str = Field(..., max_length=MAX_PAYMENT_REQUEST_LEN) - outputs: Union[List[BlindedMessage_Deprecated], None] = Field( - None, max_length=settings.mint_max_request_length - ) diff --git a/cashu/core/models/melt_quote.py b/cashu/core/models/melt_quote.py index 0ee243ad..57593b53 100644 --- a/cashu/core/models/melt_quote.py +++ b/cashu/core/models/melt_quote.py @@ -40,17 +40,11 @@ def mpp_amount(self) -> int: class PostMeltQuoteResponse(BaseModel): quote: str # quote id amount: int # input amount - unit: Optional[ - str - ] # input unit (optional for BACKWARDS COMPAT mint response < 0.17.0) - method: Optional[str] = ( - None # payment method (optional for BACKWARDS COMPAT mint response < 0.20.1) - ) - request: Optional[ - str - ] # output payment request (optional for BACKWARDS COMPAT mint response < 0.17.0) + unit: str # input unit + method: str # payment method + request: str # output payment request fee_reserve: int # input fee reserve - state: Optional[str] # state of the quote + state: str # state of the quote expiry: Optional[int] # expiry of the quote payment_preimage: Optional[str] = None # payment preimage change: Union[List[BlindedSignature], None] = None # NUT-08 change diff --git a/cashu/core/models/mint.py b/cashu/core/models/mint.py index bf0fbaeb..3dcb0a0c 100644 --- a/cashu/core/models/mint.py +++ b/cashu/core/models/mint.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field -from cashu.core.base import BlindedMessage, BlindedMessage_Deprecated, BlindedSignature +from cashu.core.base import BlindedMessage, BlindedSignature from cashu.core.constants import MAX_QUOTE_ID_LEN, MAX_SIG_LEN from cashu.core.settings import settings @@ -34,19 +34,3 @@ class PostMintBatchRequest(BaseModel): class PostMintBatchResponse(BaseModel): signatures: List[BlindedSignature] = [] - - - -class GetMintResponse_deprecated(BaseModel): - pr: str - hash: str - - -class PostMintRequest_deprecated(BaseModel): - outputs: List[BlindedMessage_Deprecated] = Field( - ..., max_length=settings.mint_max_request_length - ) - - -class PostMintResponse_deprecated(BaseModel): - promises: List[BlindedSignature] = [] diff --git a/cashu/core/models/mint_quote.py b/cashu/core/models/mint_quote.py index 7cab1e01..b400596f 100644 --- a/cashu/core/models/mint_quote.py +++ b/cashu/core/models/mint_quote.py @@ -32,19 +32,13 @@ class PostMintQuoteCheckRequest(BaseModel): class PostMintQuoteResponse(BaseModel): quote: str # quote id request: str # input payment request - amount: Optional[ - int - ] # output amount (optional for BACKWARDS COMPAT mint response < 0.17.0) - unit: Optional[ - str - ] # output unit (optional for BACKWARDS COMPAT mint response < 0.17.0) - method: Optional[str] = ( - None # payment method (optional for BACKWARDS COMPAT mint response < 0.20.1) - ) + amount: int # output amount + unit: str # output unit + method: str # payment method amount_paid: Optional[int] = None amount_issued: Optional[int] = None updated_at: Optional[int] = None - state: Optional[str] = None # state of the quote (optional for backwards compat) + state: str # state of the quote expiry: Optional[int] = None # expiry of the quote pubkey: Optional[str] = None # NUT-20 quote lock pubkey diff --git a/cashu/core/models/restore.py b/cashu/core/models/restore.py index 9b8ec5ca..6969ace0 100644 --- a/cashu/core/models/restore.py +++ b/cashu/core/models/restore.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field -from cashu.core.base import BlindedMessage, BlindedMessage_Deprecated, BlindedSignature +from cashu.core.base import BlindedMessage, BlindedSignature from cashu.core.settings import settings @@ -12,12 +12,6 @@ class PostRestoreRequest(BaseModel): ) -class PostRestoreRequest_Deprecated(BaseModel): - outputs: List[BlindedMessage_Deprecated] = Field( - ..., max_length=settings.mint_max_request_length - ) - - class PostRestoreResponse(BaseModel): outputs: List[BlindedMessage] = [] signatures: List[BlindedSignature] = [] diff --git a/cashu/core/models/swap.py b/cashu/core/models/swap.py index c6206a98..e9430421 100644 --- a/cashu/core/models/swap.py +++ b/cashu/core/models/swap.py @@ -1,10 +1,9 @@ -from typing import List, Optional +from typing import List from pydantic import BaseModel, Field from cashu.core.base import ( BlindedMessage, - BlindedMessage_Deprecated, BlindedSignature, Proof, ) @@ -20,22 +19,3 @@ class PostSwapRequest(BaseModel): class PostSwapResponse(BaseModel): signatures: List[BlindedSignature] - - -# deprecated since 0.13.0 -class PostSwapRequest_Deprecated(BaseModel): - proofs: List[Proof] = Field(..., max_length=settings.mint_max_request_length) - amount: Optional[int] = None - outputs: List[BlindedMessage_Deprecated] = Field( - ..., max_length=settings.mint_max_request_length - ) - - -class PostSwapResponse_Deprecated(BaseModel): - promises: List[BlindedSignature] = [] - - -class PostSwapResponse_Very_Deprecated(BaseModel): - fst: List[BlindedSignature] = [] - snd: List[BlindedSignature] = [] - deprecated: str = "The amount field is deprecated since 0.13.0" diff --git a/cashu/core/settings.py b/cashu/core/settings.py index a3a28ab3..3b02f669 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -43,7 +43,6 @@ class EnvSettings(CashuSettings): log_level: str = Field(default="INFO") cashu_dir: str = Field(default=os.path.join(str(Path.home()), ".cashu")) debug_profiling: bool = Field(default=False) - debug_mint_only_deprecated: bool = Field(default=False) db_backup_path: Optional[str] = Field(default=None) db_connection_pool: bool = Field(default=True) diff --git a/cashu/mint/app.py b/cashu/mint/app.py index c6c0d042..eb4cc221 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -15,7 +15,6 @@ from ..core.settings import settings from .auth.router import auth_router from .router import redis, router -from .router_deprecated import router_deprecated from .startup import ( shutdown_management_rpc, shutdown_mint, @@ -117,21 +116,8 @@ async def catch_exceptions(request: Request, call_next): # Add exception handlers app.add_exception_handler(RequestValidationError, request_validation_exception_handler) # type: ignore -def include_mint_routers(app: FastAPI) -> None: - if settings.debug_mint_only_deprecated and not settings.mint_require_auth: - app.include_router( - router=router_deprecated, tags=["Deprecated"], deprecated=True - ) - else: - app.include_router(router=router, tags=["Mint"]) - if not settings.mint_require_auth: - app.include_router( - router=router_deprecated, tags=["Deprecated"], deprecated=True - ) - - # Add routers -include_mint_routers(app) +app.include_router(router=router, tags=["Mint"]) if settings.mint_require_auth: app.include_router(auth_router, tags=["Auth"]) diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index c8367378..e93d30a3 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -195,7 +195,11 @@ async def _set_mint_quotes_pending(self, quote_ids: List[str]) -> List[MintQuote # Sort quote_ids to ensure consistent locking order sorted_quote_ids = sorted(quote_ids) lock_parameters = {f"quote_{i}": q for i, q in enumerate(sorted_quote_ids)} - lock_select_statement = "quote IN (" + ", ".join([f":quote_{i}" for i in range(len(sorted_quote_ids))]) + ")" + lock_select_statement = ( + "quote IN (" + + ", ".join([f":quote_{i}" for i in range(len(sorted_quote_ids))]) + + ")" + ) async with self.db.get_connection( lock_table="mint_quotes", @@ -214,7 +218,7 @@ async def _set_mint_quotes_pending(self, quote_ids: List[str]) -> List[MintQuote raise TransactionError(f"Mint quote {quote_id} is already issued.") if not quote.paid: raise TransactionError(f"Mint quote {quote_id} 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") @@ -271,7 +275,11 @@ async def _unset_mint_quotes_pending( quotes: List[MintQuote] = [] lock_parameters = {f"quote_{i}": q for i, q in enumerate(quote_ids)} - lock_select_statement = "quote IN (" + ", ".join([f":quote_{i}" for i in range(len(quote_ids))]) + ")" + lock_select_statement = ( + "quote IN (" + + ", ".join([f":quote_{i}" for i in range(len(quote_ids))]) + + ")" + ) async with self.db.get_connection( lock_table="mint_quotes", @@ -356,7 +364,7 @@ async def _unset_melt_quote_pending( lock_table="melt_quotes", lock_select_statement="quote = :quote", lock_parameters={"quote": quote.quote}, - conn=conn + conn=conn, ) as conn: # get melt quote from db and check if it is pending quote_db = await self.crud.get_melt_quote( @@ -368,9 +376,6 @@ async def _unset_melt_quote_pending( raise TransactionError("Melt quote not pending.") # set the quote to previous state quote_copy.state = state - - # unset outputs - quote_copy.outputs = None await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn) await self.events.submit(quote_copy) diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py deleted file mode 100644 index 5ff9b14b..00000000 --- a/cashu/mint/router_deprecated.py +++ /dev/null @@ -1,386 +0,0 @@ -from typing import Dict, List, Optional - -from fastapi import APIRouter, Request -from loguru import logger - -from ..core.base import BlindedMessage, BlindedSignature, Unit -from ..core.errors import CashuError -from ..core.models import ( - CheckFeesRequest_deprecated, - CheckFeesResponse_deprecated, - CheckSpendableRequest_deprecated, - CheckSpendableResponse_deprecated, - GetInfoResponse_deprecated, - GetMintResponse_deprecated, - KeysetsResponse_deprecated, - KeysResponse_deprecated, - PostMeltQuoteRequest, - PostMeltRequest_deprecated, - PostMeltResponse_deprecated, - PostMintQuoteRequest, - PostMintRequest_deprecated, - PostMintResponse_deprecated, - PostRestoreRequest_Deprecated, - PostRestoreResponse, - PostSwapRequest_Deprecated, - PostSwapResponse_Deprecated, - PostSwapResponse_Very_Deprecated, -) -from ..core.settings import settings -from .limit import limiter -from .startup import ledger - -router_deprecated: APIRouter = APIRouter() - - -@router_deprecated.get( - "/info", - name="Mint information", - summary="Mint information, operator contact information, and other info.", - response_model=GetInfoResponse_deprecated, - response_model_exclude_none=True, - deprecated=True, -) -async def info() -> GetInfoResponse_deprecated: - logger.trace("> GET /info") - return GetInfoResponse_deprecated( - name=settings.mint_info_name, - pubkey=ledger.pubkey.format().hex() if ledger.pubkey else None, - version=f"Nutshell/{settings.version}", - description=settings.mint_info_description, - description_long=settings.mint_info_description_long, - contact=settings.mint_info_contact, - nuts=["NUT-07", "NUT-08", "NUT-09"], - motd=settings.mint_info_motd, - ) - - -@router_deprecated.get( - "/keys", - name="Mint public keys", - summary="Get the public keys of the newest mint keyset", - response_description=( - "A dictionary of all supported token values of the mint and their associated" - " public key of the current keyset." - ), - response_model=KeysResponse_deprecated, - deprecated=True, -) -async def keys_deprecated() -> Dict[str, str]: - """This endpoint returns a dictionary of all supported token values of the mint and their associated public key.""" - logger.trace("> GET /keys") - keyset = ledger.get_keyset() - keys = KeysResponse_deprecated.model_validate( - {str(k): v for k, v in keyset.items()} - ) - return keys.root - - -@router_deprecated.get( - "/keys/{idBase64Urlsafe}", - name="Keyset public keys", - summary="Public keys of a specific keyset", - response_description=( - "A dictionary of all supported token values of the mint and their associated" - " public key for a specific keyset." - ), - response_model=Dict[str, str], - deprecated=True, -) -async def keyset_deprecated(idBase64Urlsafe: str) -> Dict[str, str]: - """ - Get the public keys of the mint from a specific keyset id. - The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to - normal base64 before it can be processed (by the mint). - """ - logger.trace(f"> GET /keys/{idBase64Urlsafe}") - id = idBase64Urlsafe.replace("-", "+").replace("_", "/") - keyset = ledger.get_keyset(keyset_id=id) - keys = KeysResponse_deprecated.model_validate( - {str(k): v for k, v in keyset.items()} - ) - return keys.root - - -@router_deprecated.get( - "/keysets", - name="Active keysets", - summary="Get all active keyset id of the mind", - response_model=KeysetsResponse_deprecated, - response_description="A list of all active keyset ids of the mint.", - deprecated=True, -) -async def keysets_deprecated() -> KeysetsResponse_deprecated: - """This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.""" - logger.trace("> GET /keysets") - sat_keysets = {k: v for k, v in ledger.keysets.items() if v.unit == Unit.sat} - keysets = KeysetsResponse_deprecated(keysets=list(sat_keysets.keys())) - return keysets - - -@router_deprecated.get( - "/mint", - name="Request mint", - summary="Request minting of new tokens", - response_model=GetMintResponse_deprecated, - response_description=( - "A Lightning invoice to be paid and a hash to request minting of new tokens" - " after payment." - ), - deprecated=True, -) -@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def request_mint_deprecated( - request: Request, amount: int = 0 -) -> GetMintResponse_deprecated: - """ - Request minting of new tokens. The mint responds with a Lightning invoice. - This endpoint can be used for a Lightning invoice UX flow. - - Call `POST /mint` after paying the invoice. - """ - logger.trace(f"> GET /mint: amount={amount}") - if amount > 21_000_000 * 100_000_000 or amount <= 0: - raise CashuError(code=0, detail="Amount must be a valid amount of sat.") - if settings.mint_bolt11_disable_mint: - raise CashuError(code=0, detail="Mint does not allow minting new tokens.") - quote = await ledger.mint_quote( - PostMintQuoteRequest(amount=amount, unit=Unit.sat.name) - ) - resp = GetMintResponse_deprecated(pr=quote.request, hash=quote.quote) - logger.trace(f"< GET /mint: {resp}") - return resp - - -@router_deprecated.post( - "/mint", - name="Mint tokens", - summary="Mint tokens in exchange for a Bitcoin payment that the user has made", - response_model=PostMintResponse_deprecated, - response_description=( - "A list of blinded signatures that can be used to create proofs." - ), - deprecated=True, -) -@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def mint_deprecated( - request: Request, - payload: PostMintRequest_deprecated, - hash: Optional[str] = None, - payment_hash: Optional[str] = None, -) -> PostMintResponse_deprecated: - """ - Requests the minting of tokens belonging to a paid payment request. - - Call this endpoint after `GET /mint`. - """ - logger.trace(f"> POST /mint: {payload}") - - # BEGIN BACKWARDS COMPATIBILITY < 0.15 - # Mint expects "id" in outputs to know which keyset to use to sign them. - outputs: list[BlindedMessage] = [ - BlindedMessage(id=ledger.keyset.id, **o.model_dump(exclude={"id"})) - for o in payload.outputs - ] - # END BACKWARDS COMPATIBILITY < 0.15 - - # BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash - # We use the payment_hash to lookup the hash from the database and pass that one along. - hash = payment_hash or hash - assert hash, "hash must be set." - # END: backwards compatibility < 0.12 - - promises = await ledger.mint(outputs=outputs, quote_id=hash) - blinded_signatures = PostMintResponse_deprecated(promises=promises) - - logger.trace(f"< POST /mint: {blinded_signatures}") - return blinded_signatures - - -@router_deprecated.post( - "/melt", - name="Melt tokens", - summary=( - "Melt tokens for a Bitcoin payment that the mint will make for the user in" - " exchange" - ), - response_model=PostMeltResponse_deprecated, - response_description=( - "The state of the payment, a preimage as proof of payment, and a list of" - " promises for change." - ), - deprecated=True, -) -@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def melt_deprecated( - request: Request, - payload: PostMeltRequest_deprecated, -) -> PostMeltResponse_deprecated: - """ - Requests tokens to be destroyed and sent out via Lightning. - """ - logger.trace(f"> POST /melt: {payload}") - # BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs - if payload.outputs: - outputs: list[BlindedMessage] = [ - BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"})) - for o in payload.outputs - ] - else: - outputs = [] - # END BACKWARDS COMPATIBILITY < 0.14 - quote = await ledger.melt_quote( - PostMeltQuoteRequest(request=payload.pr, unit=Unit.sat.name) - ) - melt_resp = await ledger.melt( - proofs=payload.proofs, quote=quote.quote, outputs=outputs - ) - resp = PostMeltResponse_deprecated( - paid=True, preimage=melt_resp.payment_preimage, change=melt_resp.change - ) - logger.trace(f"< POST /melt: {resp}") - return resp - - -@router_deprecated.post( - "/checkfees", - name="Check fees", - summary="Check fee reserve for a Lightning payment", - response_model=CheckFeesResponse_deprecated, - response_description="The fees necessary to pay a Lightning invoice.", - deprecated=True, -) -async def check_fees( - payload: CheckFeesRequest_deprecated, -) -> CheckFeesResponse_deprecated: - """ - Responds with the fees necessary to pay a Lightning invoice. - Used by wallets for figuring out the fees they need to supply together with the payment amount. - This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). - """ - logger.trace(f"> POST /checkfees: {payload}") - quote = await ledger.melt_quote( - PostMeltQuoteRequest(request=payload.pr, unit=Unit.sat.name) - ) - fees_sat = quote.fee_reserve - logger.trace(f"< POST /checkfees: {fees_sat}") - return CheckFeesResponse_deprecated(fee=fees_sat) - - -@router_deprecated.post( - "/split", - name="Split", - summary="Split proofs at a specified amount", - # response_model=Union[ - # PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated - # ], - response_description=( - "A list of blinded signatures that can be used to create proofs." - ), - deprecated=True, -) -@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def split_deprecated( - request: Request, - payload: PostSwapRequest_Deprecated, - # ) -> Union[PostSwapResponse_Very_Deprecated, PostSwapResponse_Deprecated]: -): - """ - Requests a set of Proofs to be split into two a new set of BlindedSignatures. - - This endpoint is used by Alice to split a set of proofs before making a payment to Carol. - It is then used by Carol (by setting split=total) to redeem the tokens. - """ - logger.trace(f"> POST /split: {payload}") - assert payload.outputs, Exception("no outputs provided.") - # BEGIN BACKWARDS COMPATIBILITY < 0.14: add "id" to outputs - outputs: list[BlindedMessage] = [ - BlindedMessage(id=ledger.keyset.id, **o.model_dump(exclude={"id"})) - for o in payload.outputs - ] - # END BACKWARDS COMPATIBILITY < 0.14 - promises = await ledger.swap(proofs=payload.proofs, outputs=outputs) - - if payload.amount: - # BEGIN backwards compatibility < 0.13 - # old clients expect two lists of promises where the second one's amounts - # sum up to `amount`. The first one is the rest. - # The returned value `promises` has the form [keep1, keep2, ..., send1, send2, ...] - # The sum of the sendx is `amount`. We need to split this into two lists and keep the order of the elements. - frst_promises: List[BlindedSignature] = [] - scnd_promises: List[BlindedSignature] = [] - scnd_amount = 0 - for promise in promises[::-1]: # we iterate backwards - if scnd_amount < payload.amount: - scnd_promises.insert(0, promise) # and insert at the beginning - scnd_amount += promise.amount - else: - frst_promises.insert(0, promise) # and insert at the beginning - logger.trace( - f"Split into keep: {len(frst_promises)}:" - f" {sum([p.amount for p in frst_promises])} sat and send:" - f" {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat" - ) - return PostSwapResponse_Very_Deprecated(fst=frst_promises, snd=scnd_promises) - # END backwards compatibility < 0.13 - else: - return PostSwapResponse_Deprecated(promises=promises) - - -@router_deprecated.post( - "/check", - name="Check proof state", - summary="Check whether a proof is spent already or is pending in a transaction", - response_model=CheckSpendableResponse_deprecated, - response_description=( - "Two lists of booleans indicating whether the provided proofs " - "are spendable or pending in a transaction respectively." - ), - deprecated=True, -) -async def check_spendable_deprecated( - payload: CheckSpendableRequest_deprecated, -) -> CheckSpendableResponse_deprecated: - """Check whether a secret has been spent already or not.""" - logger.trace(f"> POST /check: {payload}") - proofs_state = await ledger.db_read.get_proofs_states([p.Y for p in payload.proofs]) - spendableList: List[bool] = [] - pendingList: List[bool] = [] - for proof_state in proofs_state: - if proof_state.unspent: - spendableList.append(True) - pendingList.append(False) - elif proof_state.spent: - spendableList.append(False) - pendingList.append(False) - elif proof_state.pending: - spendableList.append(True) - pendingList.append(True) - return CheckSpendableResponse_deprecated( - spendable=spendableList, pending=pendingList - ) - - -@router_deprecated.post( - "/restore", - name="Restore", - summary="Restores a blinded signature from a secret", - response_model=PostRestoreResponse, - response_description=( - "Two lists with the first being the list of the provided outputs that " - "have an associated blinded signature which is given in the second list." - ), - deprecated=True, -) -async def restore(payload: PostRestoreRequest_Deprecated) -> PostRestoreResponse: - assert payload.outputs, Exception("no outputs provided.") - if payload.outputs: - outputs: list[BlindedMessage] = [ - BlindedMessage(id=ledger.keyset.id, **o.dict(exclude={"id"})) - for o in payload.outputs - ] - else: - outputs = [] - - outputs, promises = await ledger.restore(outputs) - return PostRestoreResponse(outputs=outputs, signatures=promises) diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index f5d65b04..18089b5d 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -6,17 +6,12 @@ import httpx from httpx import Response from loguru import logger -from pydantic import ValidationError from ..core.base import ( AuthProof, BlindedMessage, BlindedSignature, - MeltQuoteState, - Method, Proof, - ProofSpentState, - ProofState, Unit, WalletKeyset, ) @@ -37,7 +32,6 @@ PostMeltRequest, PostMeltRequestOptionMpp, PostMeltRequestOptions, - PostMeltResponse_deprecated, PostMintQuoteRequest, PostMintQuoteResponse, PostMintRequest, @@ -545,36 +539,8 @@ def _meltrequest_include_fields( ), # type: ignore timeout=None, ) - try: - self.raise_on_error_request(resp) - return_dict = resp.json() - return PostMeltQuoteResponse.model_validate(return_dict) - except Exception as e: - # BEGIN backwards compatibility < 0.15.0 - # before 0.16.0, mints return PostMeltResponse_deprecated - if isinstance(e, ValidationError): - # BEGIN backwards compatibility < 0.16.0 - ret = PostMeltResponse_deprecated.model_validate(return_dict) - # END backwards compatibility < 0.16.0 - else: - raise e - return PostMeltQuoteResponse( - quote=quote, - amount=0, - unit=Unit.sat.name, - method=Method.bolt11.name, - request="lnbc0", - fee_reserve=0, - state=( - MeltQuoteState.paid.value - if ret.paid - else MeltQuoteState.unpaid.value - ), - payment_preimage=ret.preimage, - change=ret.change, - expiry=None, - ) - # END backwards compatibility < 0.15.0 + self.raise_on_error_request(resp) + return PostMeltQuoteResponse.model_validate(resp.json()) @async_set_httpx_client @async_ensure_mint_loaded @@ -636,23 +602,6 @@ async def check_proof_state(self, proofs: List[Proof]) -> PostCheckStateResponse # fail if endpoint missing self.raise_on_unsupported_version(resp, "POST /v1/checkstate") - - # BEGIN backwards compatibility < 0.16.0 - # payload has "secrets" instead of "Ys" - if resp.status_code == 422: - logger.warning( - "Received HTTP Error 422. Attempting state check with < 0.16.0 compatibility." - ) - payload_secrets = {"secrets": [p.secret for p in proofs]} - resp_secrets = await self._request(POST, "checkstate", json=payload_secrets) - self.raise_on_error_request(resp_secrets) - states = [ - ProofState(Y=p.Y, state=ProofSpentState(s["state"])) - for p, s in zip(proofs, resp_secrets.json()["states"]) - ] - return PostCheckStateResponse(states=states) - # END backwards compatibility < 0.16.0 - self.raise_on_error_request(resp) return PostCheckStateResponse.model_validate(resp.json()) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index e666ea55..c7b04142 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -535,7 +535,7 @@ async def request_mint_with_callback( filters=[mint_quote.quote], callback=callback, ) - quote = MintQuote.from_resp_wallet(mint_quote, self.url, amount, self.unit.name) + quote = MintQuote.from_resp_wallet(mint_quote, self.url) # store the private key in the quote quote.privkey = privkey_hex @@ -578,9 +578,7 @@ async def request_mint( mint_quote_response = await super().mint_quote( amount, self.unit, memo, pubkey_hex ) - quote = MintQuote.from_resp_wallet( - mint_quote_response, self.url, amount, self.unit.name - ) + quote = MintQuote.from_resp_wallet(mint_quote_response, self.url) quote.privkey = privkey_hex await store_bolt11_mint_quote(db=self.db, quote=quote) @@ -605,8 +603,6 @@ async def get_mint_quote( mint_quote_resp=mint_quote_response, mint=self.url, mint_quote_local=mint_quote_local, - default_amount=0, - default_unit=self.unit.name, ) if mint_quote_local and mint_quote_local.privkey: @@ -841,21 +837,8 @@ async def melt_quote( logger.debug( f"Mint wants {self.unit.str(melt_quote_resp.fee_reserve)} as fee reserve." ) - melt_quote = MeltQuote.from_resp_wallet( - melt_quote_resp, - self.url, - unit=self.unit.name, - request=invoice, - ) + melt_quote = MeltQuote.from_resp_wallet(melt_quote_resp, self.url) await store_bolt11_melt_quote(db=self.db, quote=melt_quote) - melt_quote = MeltQuote.from_resp_wallet( - melt_quote_resp, - self.url, - unit=melt_quote_resp.unit - or self.unit.name, # BACKWARD COMPATIBILITY mint response < 0.17.0 - request=melt_quote_resp.request - or invoice, # BACKWARD COMPATIBILITY mint response < 0.17.0 - ) return melt_quote async def get_melt_quote(self, quote: str) -> Optional[MeltQuote]: @@ -869,20 +852,7 @@ async def get_melt_quote(self, quote: str) -> Optional[MeltQuote]: """ melt_quote_resp = await super().get_melt_quote(quote) melt_quote_local = await get_bolt11_melt_quote(db=self.db, quote=quote) - melt_quote = MeltQuote.from_resp_wallet( - melt_quote_resp, - self.url, - unit=( - melt_quote_resp.unit or melt_quote_local.unit - if melt_quote_local - else self.unit.name # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - request=( - melt_quote_resp.request or melt_quote_local.request - if (melt_quote_local and melt_quote_local.request) - else "None" # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - ) + melt_quote = MeltQuote.from_resp_wallet(melt_quote_resp, self.url) # update database if not melt_quote_local: @@ -961,12 +931,7 @@ async def melt( await self.set_reserved_for_melt(proofs, reserved=False, quote_id=None) raise Exception(f"could not pay invoice: {e}") - melt_quote = MeltQuote.from_resp_wallet( - melt_quote_resp, - self.url, - unit=self.unit.name, - request=invoice, - ) + melt_quote = MeltQuote.from_resp_wallet(melt_quote_resp, self.url) # if payment fails if melt_quote.state == MeltQuoteState.unpaid: # remove the melt_id in proofs and set reserved to False diff --git a/tests/fuzz/test_fuzz_mint_api.py b/tests/fuzz/test_fuzz_mint_api.py index e8500391..282d0c48 100644 --- a/tests/fuzz/test_fuzz_mint_api.py +++ b/tests/fuzz/test_fuzz_mint_api.py @@ -10,7 +10,6 @@ Proof, ) from cashu.mint.app import app -from tests.helpers import is_deprecated_api_only ALLOWED_STATUS_CODES = [400, 404, 405, 422, 503] @@ -56,9 +55,6 @@ def client(): with TestClient(app) as c: yield c -# Apply skip marker to all tests in this module if deprecated API is enabled -pytestmark = pytest.mark.skipif(is_deprecated_api_only, reason="Deprecated API enabled") - # Fuzzers @hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) diff --git a/tests/fuzz/test_fuzz_mint_api_deprecated.py b/tests/fuzz/test_fuzz_mint_api_deprecated.py deleted file mode 100644 index 69d69cbe..00000000 --- a/tests/fuzz/test_fuzz_mint_api_deprecated.py +++ /dev/null @@ -1,160 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from hypothesis import HealthCheck, given -from hypothesis import settings as hypothesis_settings -from hypothesis import strategies as st - -from cashu.core.base import ( - BlindedMessage, - Proof, -) -from cashu.mint.app import app -from tests.helpers import is_deprecated_api_only - -# Apply skip marker to all tests in this module if deprecated API is NOT enabled -pytestmark = pytest.mark.skipif(not is_deprecated_api_only, reason="Deprecated API not enabled") - -@pytest.fixture -def client(): - with TestClient(app) as c: - yield c - -# Define strategies -def hex_string(min_len=66, max_len=66): - return st.text(alphabet="0123456789abcdef", min_size=min_len, max_size=max_len) - -def valid_unit(): - return st.sampled_from(["sat", "usd", "eur", "msat"]) - -def public_key(): - return st.builds(lambda a, b: a + b, st.just("02"), hex_string(64, 64)) - -def keyset_id(): - return st.builds(lambda a, b: a + b, st.just("00"), hex_string(14, 14)) - -def url_safe_text(min_len=1, max_len=20): - # Generates text that is safe for URLs (no control chars, no slashes, no #, no ?) - return st.text(alphabet=st.characters(blacklist_categories=("Cc", "Cs", "Zl", "Zp", "Cn")), min_size=min_len, max_size=max_len).filter(lambda x: "/" not in x and "\\" not in x and "#" not in x and "?" not in x) - -# Strategy for BlindedMessage -blinded_message_strategy = st.builds( - BlindedMessage, - amount=st.integers(min_value=1), - id=st.one_of(keyset_id(), hex_string(16, 16)), - B_=st.one_of(hex_string(), public_key()), - C_=st.one_of(st.none(), public_key(), hex_string()) -) - -# Strategy for Proof -proof_strategy = st.builds( - Proof, - id=st.one_of(keyset_id(), hex_string(16, 16)), - amount=st.integers(min_value=1), - secret=st.text(min_size=1, max_size=64), - C=st.one_of(hex_string(), public_key()), - Y=st.one_of(public_key(), hex_string()) -) - -# Fuzzers for Deprecated API - -# GET /mint -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given(amount=st.integers()) -def test_fuzz_deprecated_mint_get(client, amount): - response = client.get(f"/mint?amount={amount}") - # Expecting either success or valid error codes - assert response.status_code in [200, 400, 404, 422, 503] - -# POST /mint -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given( - outputs=st.lists(blinded_message_strategy, min_size=1, max_size=10), - hash=st.one_of(st.none(), st.text(min_size=1, max_size=64)) -) -def test_fuzz_deprecated_mint_post(client, outputs, hash): - outputs_json = [o.model_dump() for o in outputs] - payload = {"outputs": outputs_json} - params = {} - if hash: - params["hash"] = hash - - response = client.post("/mint", json=payload, params=params) - assert response.status_code in [400, 404, 422, 503] - -# POST /melt -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given( - pr=st.text(min_size=1, max_size=500), - proofs=st.lists(proof_strategy, min_size=1, max_size=10), - outputs=st.one_of(st.none(), st.lists(blinded_message_strategy, min_size=1, max_size=5)) -) -def test_fuzz_deprecated_melt(client, pr, proofs, outputs): - inputs_json = [p.model_dump(exclude={'dleq', 'witness'}) for p in proofs] - outputs_json = [o.model_dump() for o in outputs] if outputs else None - - payload = { - "pr": pr, - "proofs": inputs_json, - "outputs": outputs_json - } - response = client.post("/melt", json=payload) - assert response.status_code in [400, 404, 422, 503] - -# POST /check -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given( - proofs=st.lists(proof_strategy, min_size=1, max_size=10) -) -def test_fuzz_deprecated_check(client, proofs): - inputs_json = [p.model_dump(exclude={'dleq', 'witness'}) for p in proofs] - payload = {"proofs": inputs_json} - - response = client.post("/check", json=payload) - if response.status_code == 200: - data = response.json() - # For random proofs (unknown to mint), they are considered UNSPENT - # So spendable should be True, pending should be False - assert len(data["spendable"]) == len(proofs) - assert len(data["pending"]) == len(proofs) - assert all(data["spendable"]) - assert not any(data["pending"]) - else: - assert response.status_code in [400, 404, 422, 503] - -# POST /split -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given( - proofs=st.lists(proof_strategy, min_size=1, max_size=10), - outputs=st.lists(blinded_message_strategy, min_size=1, max_size=10), - amount=st.one_of(st.none(), st.integers(min_value=1)) -) -def test_fuzz_deprecated_split(client, proofs, outputs, amount): - inputs_json = [p.model_dump(exclude={'dleq', 'witness'}) for p in proofs] - outputs_json = [o.model_dump() for o in outputs] - - payload = { - "proofs": inputs_json, - "outputs": outputs_json - } - if amount is not None: - payload["amount"] = amount - - response = client.post("/split", json=payload) - assert response.status_code in [400, 404, 422, 503] - -# POST /restore -@hypothesis_settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50) -@given( - outputs=st.lists(blinded_message_strategy, min_size=1, max_size=20) -) -def test_fuzz_deprecated_restore(client, outputs): - outputs_json = [o.model_dump() for o in outputs] - payload = {"outputs": outputs_json} - - response = client.post("/restore", json=payload) - if response.status_code == 200: - data = response.json() - assert data["outputs"] == [] - assert data["signatures"] == [] - else: - assert response.status_code in [400, 404, 422, 503] diff --git a/tests/helpers.py b/tests/helpers.py index 2e795967..9afe8921 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -66,7 +66,6 @@ async def get_random_invoice_data(): WALLET = wallet_class(unit=Unit.sat) is_fake: bool = WALLET.__class__.__name__ == "FakeWallet" is_regtest: bool = not is_fake -is_deprecated_api_only = settings.debug_mint_only_deprecated is_github_actions = os.getenv("GITHUB_ACTIONS") == "true" is_postgres = settings.mint_database.startswith("postgres") SLEEP_TIME = 1 if not is_github_actions else 2 diff --git a/tests/mint/test_mint_api.py b/tests/mint/test_mint_api.py index 5a361e45..ee86b261 100644 --- a/tests/mint/test_mint_api.py +++ b/tests/mint/test_mint_api.py @@ -16,7 +16,6 @@ ) from cashu.core.nuts import nut20 from cashu.core.nuts.nuts import MINT_NUT -from cashu.core.settings import settings from cashu.mint.ledger import Ledger from cashu.wallet.crud import bump_secret_derivation from cashu.wallet.wallet import Wallet @@ -45,10 +44,6 @@ async def test_landing_page(): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_info(ledger: Ledger): response = httpx.get(f"{BASE_URL}/v1/info") assert response.status_code == 200, f"{response.url} {response.status_code}" @@ -66,10 +61,6 @@ async def test_info(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_keys(ledger: Ledger): response = httpx.get(f"{BASE_URL}/v1/keys") assert response.status_code == 200, f"{response.url} {response.status_code}" @@ -94,10 +85,6 @@ async def test_api_keys(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_keysets(ledger: Ledger): response = httpx.get(f"{BASE_URL}/v1/keysets") assert response.status_code == 200, f"{response.url} {response.status_code}" @@ -123,10 +110,6 @@ async def test_api_keysets(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_keyset_keys(ledger: Ledger): response = httpx.get( f"{BASE_URL}/v1/keys/01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc" @@ -154,10 +137,6 @@ async def test_api_keyset_keys(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_keyset_keys_old_keyset_id(ledger: Ledger): response = httpx.get( f"{BASE_URL}/v1/keys/01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc" @@ -185,10 +164,6 @@ async def test_api_keyset_keys_old_keyset_id(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_swap(ledger: Ledger, wallet: Wallet): mint_quote = await wallet.request_mint(64) await pay_if_regtest(mint_quote.request) @@ -216,10 +191,6 @@ async def test_swap(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_quote(ledger: Ledger): response = httpx.post( f"{BASE_URL}/v1/mint/quote/bolt11", @@ -280,10 +251,6 @@ async def test_mint_quote(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint(ledger: Ledger, wallet: Wallet): mint_quote = await wallet.request_mint(64) await pay_if_regtest(mint_quote.request) @@ -316,10 +283,6 @@ async def test_mint(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_bolt11_no_signature(ledger: Ledger, wallet: Wallet): """ For backwards compatibility, we do not require a NUT-20 signature @@ -354,10 +317,6 @@ async def test_mint_bolt11_no_signature(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) @pytest.mark.skipif( is_regtest, reason="regtest", @@ -398,10 +357,6 @@ async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) @pytest.mark.skipif( is_fake, reason="only works on regtest", @@ -423,10 +378,6 @@ async def test_melt_quote_external(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_melt_internal(ledger: Ledger, wallet: Wallet): # internal invoice mint_quote = await wallet.request_mint(64) @@ -477,10 +428,6 @@ async def test_melt_internal(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) @pytest.mark.skipif( is_fake, reason="only works on regtest", @@ -538,10 +485,6 @@ async def test_melt_external(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_check_state(ledger: Ledger): payload = PostCheckStateRequest(Ys=["asdasdasd", "asdasdasd1"]) response = httpx.post( @@ -556,10 +499,6 @@ async def test_api_check_state(ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_api_restore(ledger: Ledger, wallet: Wallet): mint_quote = await wallet.request_mint(64) await pay_if_regtest(mint_quote.request) @@ -597,10 +536,6 @@ async def test_api_restore(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_quote_check(ledger: Ledger, wallet: Wallet): mint_quote1 = await wallet.request_mint(64) mint_quote2 = await wallet.request_mint(32) @@ -623,10 +558,6 @@ async def test_mint_quote_check(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_batch_success(ledger: Ledger, wallet: Wallet): mint_quote1 = await wallet.request_mint(64) mint_quote2 = await wallet.request_mint(32) @@ -658,9 +589,9 @@ async def test_mint_batch_success(ledger: Ledger, wallet: Wallet): timeout=None, ) - assert ( - response.status_code == 200 - ), f"{response.url} {response.status_code} {response.text}" + assert response.status_code == 200, ( + f"{response.url} {response.status_code} {response.text}" + ) result = response.json() assert len(result["signatures"]) == 2 assert result["signatures"][0]["amount"] == 64 @@ -668,10 +599,6 @@ async def test_mint_batch_success(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_batch_duplicate_quotes(ledger: Ledger, wallet: Wallet): mint_quote1 = await wallet.request_mint(64) @@ -690,10 +617,6 @@ async def test_mint_batch_duplicate_quotes(ledger: Ledger, wallet: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_mint_batch_wrong_amount(ledger: Ledger, wallet: Wallet): mint_quote1 = await wallet.request_mint(64) await pay_if_regtest(mint_quote1.request) diff --git a/tests/mint/test_mint_api_deprecated.py b/tests/mint/test_mint_api_deprecated.py deleted file mode 100644 index 136cc516..00000000 --- a/tests/mint/test_mint_api_deprecated.py +++ /dev/null @@ -1,320 +0,0 @@ -import httpx -import pytest -import pytest_asyncio - -from cashu.core.base import Proof, Unit -from cashu.core.models import ( - CheckSpendableRequest_deprecated, - CheckSpendableResponse_deprecated, - GetMintResponse_deprecated, -) -from cashu.mint.ledger import Ledger -from cashu.wallet.wallet import Wallet -from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest - -pytestmark = pytest.mark.skip(reason="Wallet v0 / deprecated mint API removed in https://github.com/cashubtc/nutshell/pull/814") - -BASE_URL = "http://localhost:3337" - - -@pytest_asyncio.fixture(scope="function") -async def wallet(ledger: Ledger): - wallet1 = await Wallet.with_db( - url=BASE_URL, - db="test_data/wallet_mint_api_deprecated", - name="wallet_mint_api_deprecated", - ) - await wallet1.load_mint() - yield wallet1 - - -@pytest.mark.asyncio -async def test_info(ledger: Ledger): - response = httpx.get(f"{BASE_URL}/info") - assert response.status_code == 200, f"{response.url} {response.status_code}" - assert ledger.pubkey - assert response.json()["pubkey"] == ledger.pubkey.format().hex() - - -@pytest.mark.asyncio -async def test_api_keys(ledger: Ledger): - response = httpx.get(f"{BASE_URL}/keys") - assert response.status_code == 200, f"{response.url} {response.status_code}" - assert ledger.keyset.public_keys - assert response.json() == { - str(k): v.format().hex() for k, v in ledger.keyset.public_keys.items() - } - - -@pytest.mark.asyncio -async def test_api_keysets(ledger: Ledger): - response = httpx.get(f"{BASE_URL}/keysets") - assert response.status_code == 200, f"{response.url} {response.status_code}" - assert ledger.keyset.public_keys - sat_keysets = {k: v for k, v in ledger.keysets.items() if v.unit == Unit.sat} - assert response.json()["keysets"] == list(sat_keysets.keys()) - - -@pytest.mark.asyncio -async def test_api_keyset_keys(ledger: Ledger): - response = httpx.get(f"{BASE_URL}/keys/01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc") - assert response.status_code == 200, f"{response.url} {response.status_code}" - assert ledger.keyset.public_keys - assert response.json() == { - str(k): v.format().hex() for k, v in ledger.keyset.public_keys.items() - } - - -@pytest.mark.asyncio -async def test_split(ledger: Ledger, wallet: Wallet): - mint_quote = await wallet.request_mint(64) - await pay_if_regtest(mint_quote.request) - await wallet.mint(64, quote_id=mint_quote.quote) - assert wallet.balance == 64 - secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(20000, 20001) - outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) - # outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"]) - inputs_payload = [p.to_dict() for p in wallet.proofs] - outputs_payload = [o.model_dump() for o in outputs] - # strip "id" from outputs_payload, which is not used in the deprecated split endpoint - for o in outputs_payload: - o.pop("id") - payload = {"proofs": inputs_payload, "outputs": outputs_payload} - response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - assert result["promises"] - - -@pytest.mark.asyncio -async def test_split_deprecated_with_amount(ledger: Ledger, wallet: Wallet): - mint_quote = await wallet.request_mint(64) - await pay_if_regtest(mint_quote.request) - await wallet.mint(64, quote_id=mint_quote.quote) - assert wallet.balance == 64 - secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(80000, 80001) - outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) - # outputs = wallet._construct_outputs([32, 32], ["a", "b"], ["c", "d"]) - inputs_payload = [p.to_dict() for p in wallet.proofs] - outputs_payload = [o.model_dump() for o in outputs] - # strip "id" from outputs_payload, which is not used in the deprecated split endpoint - for o in outputs_payload: - o.pop("id") - # we supply an amount here, which should cause the very old deprecated split endpoint to be used - payload = {"proofs": inputs_payload, "outputs": outputs_payload, "amount": 32} - response = httpx.post(f"{BASE_URL}/split", json=payload, timeout=None) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - # old deprecated output format - assert result["fst"] - assert result["snd"] - - -@pytest.mark.asyncio -async def test_api_mint_validation(ledger): - response = httpx.get(f"{BASE_URL}/mint?amount=-21") - assert "detail" in response.json() - response = httpx.get(f"{BASE_URL}/mint?amount=0") - assert "detail" in response.json() - response = httpx.get(f"{BASE_URL}/mint?amount=2100000000000001") - assert "detail" in response.json() - response = httpx.get(f"{BASE_URL}/mint?amount=1") - assert "detail" not in response.json() - - -@pytest.mark.asyncio -async def test_mint(ledger: Ledger, wallet: Wallet): - quote_response = httpx.get( - f"{BASE_URL}/mint", - params={"amount": 64}, - timeout=None, - ) - mint_quote = GetMintResponse_deprecated.model_validate(quote_response.json()) - await pay_if_regtest(mint_quote.pr) - secrets, rs, derivation_paths = await wallet.generate_secrets_from_to(10000, 10001) - outputs, rs = wallet._construct_outputs([32, 32], secrets, rs) - outputs_payload = [o.model_dump() for o in outputs] - response = httpx.post( - f"{BASE_URL}/mint", - json={"outputs": outputs_payload}, - params={"hash": mint_quote.hash}, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - assert len(result["promises"]) == 2 - assert result["promises"][0]["amount"] == 32 - assert result["promises"][1]["amount"] == 32 - assert result["promises"][0]["id"] == "01d8a63077d0a51f9855f066409782ffcb322dc8a2265291865221ed06c039f6bc" - assert result["promises"][0]["dleq"] - assert "e" in result["promises"][0]["dleq"] - assert "s" in result["promises"][0]["dleq"] - - -@pytest.mark.asyncio -async def test_melt_internal(ledger: Ledger, wallet: Wallet): - # fill wallet - mint_quote = await wallet.request_mint(64) - await pay_if_regtest(mint_quote.request) - await wallet.mint(64, quote_id=mint_quote.quote) - assert wallet.balance == 64 - - # create invoice to melt to - mint_quote = await wallet.request_mint(64) - - invoice_payment_request = mint_quote.request - - quote = await wallet.melt_quote(invoice_payment_request) - assert quote.amount == 64 - assert quote.fee_reserve == 0 - - inputs_payload = [p.to_dict() for p in wallet.proofs] - - # outputs for change - secrets, rs, derivation_paths = await wallet.generate_n_secrets(1) - outputs, rs = wallet._construct_outputs([2], secrets, rs) - outputs_payload = [o.model_dump() for o in outputs] - - response = httpx.post( - f"{BASE_URL}/melt", - json={ - "pr": invoice_payment_request, - "proofs": inputs_payload, - "outputs": outputs_payload, - }, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - assert result.get("preimage") is None - -@pytest.mark.asyncio -async def test_melt_internal_no_change_outputs(ledger: Ledger, wallet: Wallet): - # Clients without NUT-08 will not send change outputs - # internal invoice - mint_quote = await wallet.request_mint(64) - await pay_if_regtest(mint_quote.request) - await wallet.mint(64, quote_id=mint_quote.quote) - assert wallet.balance == 64 - - # create invoice to melt to - mint_quote = await wallet.request_mint(64) - invoice_payment_request = mint_quote.request - quote = await wallet.melt_quote(invoice_payment_request) - assert quote.amount == 64 - assert quote.fee_reserve == 0 - - inputs_payload = [p.to_dict() for p in wallet.proofs] - - # outputs for change - secrets, rs, derivation_paths = await wallet.generate_n_secrets(1) - outputs, rs = wallet._construct_outputs([2], secrets, rs) - - response = httpx.post( - f"{BASE_URL}/melt", - json={ - "pr": invoice_payment_request, - "proofs": inputs_payload, - }, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - assert result.get("preimage") is None - -@pytest.mark.asyncio -@pytest.mark.skipif( - is_fake, - reason="only works on regtest", -) -async def test_melt_external(ledger: Ledger, wallet: Wallet): - # internal invoice - mint_quote = await wallet.request_mint(64) - await pay_if_regtest(mint_quote.request) - await wallet.mint(64, quote_id=mint_quote.quote) - assert wallet.balance == 64 - - # create invoice to melt to - # use 2 sat less because we need to pay the fee - invoice_dict = get_real_invoice(62) - invoice_payment_request = invoice_dict["payment_request"] - - quote = await wallet.melt_quote(invoice_payment_request) - assert quote.amount == 62 - assert quote.fee_reserve == 2 - - inputs_payload = [p.to_dict() for p in wallet.proofs] - - # outputs for change - secrets, rs, derivation_paths = await wallet.generate_n_secrets(1) - outputs, rs = wallet._construct_outputs([2], secrets, rs) - outputs_payload = [o.model_dump() for o in outputs] - - response = httpx.post( - f"{BASE_URL}/melt", - json={ - "pr": invoice_payment_request, - "proofs": inputs_payload, - "outputs": outputs_payload, - }, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - assert result.get("preimage") is not None - assert result["change"] - # we get back 2 sats because Lightning was free to pay on regtest - assert result["change"][0]["amount"] == 2 - -@pytest.mark.asyncio -async def test_checkfees(ledger: Ledger, wallet: Wallet): - # internal invoice - mint_quote = await wallet.request_mint(64) - response = httpx.post( - f"{BASE_URL}/checkfees", - json={ - "pr": mint_quote.request, - }, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - # internal invoice, so no fee - assert result["fee"] == 0 - - -@pytest.mark.asyncio -@pytest.mark.skipif(not is_regtest, reason="only works on regtest") -async def test_checkfees_external(ledger: Ledger, wallet: Wallet): - # external invoice - invoice_dict = get_real_invoice(62) - invoice_payment_request = invoice_dict["payment_request"] - response = httpx.post( - f"{BASE_URL}/checkfees", - json={"pr": invoice_payment_request}, - timeout=None, - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - result = response.json() - # external invoice, so fee - assert result["fee"] == 2 - - -@pytest.mark.asyncio -async def test_api_check_state(ledger: Ledger): - proofs = [ - Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"), - Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"), - ] - payload = CheckSpendableRequest_deprecated(proofs=proofs) - response = httpx.post( - f"{BASE_URL}/check", - json=payload.model_dump(), - ) - assert response.status_code == 200, f"{response.url} {response.status_code}" - states = CheckSpendableResponse_deprecated.model_validate(response.json()) - assert states.spendable - assert len(states.spendable) == 2 - assert states.pending - assert len(states.pending) == 2 - diff --git a/tests/mint/test_mint_app.py b/tests/mint/test_mint_app.py deleted file mode 100644 index dc9962ec..00000000 --- a/tests/mint/test_mint_app.py +++ /dev/null @@ -1,22 +0,0 @@ -from fastapi import FastAPI - -from cashu.core.settings import settings -from cashu.mint.app import include_mint_routers - - -def test_auth_enabled_does_not_mount_deprecated_transaction_routes(monkeypatch): - monkeypatch.setattr(settings, "mint_require_auth", True) - monkeypatch.setattr(settings, "debug_mint_only_deprecated", False) - test_app = FastAPI() - - include_mint_routers(test_app) - - mounted_routes = { - (method, route.path) - for route in test_app.routes - for method in getattr(route, "methods", set()) - } - assert ("POST", "/v1/swap") in mounted_routes - assert ("POST", "/mint") not in mounted_routes - assert ("POST", "/melt") not in mounted_routes - assert ("POST", "/split") not in mounted_routes diff --git a/tests/mint/test_mint_app_router.py b/tests/mint/test_mint_app_router.py index 86de05aa..462bc418 100644 --- a/tests/mint/test_mint_app_router.py +++ b/tests/mint/test_mint_app_router.py @@ -163,6 +163,25 @@ def test_create_app_sets_metadata(): assert app.version == settings.version +def test_app_does_not_mount_v0_mint_routes(): + paths = {route.path for route in app_module.app.routes} + assert "/v1/info" in paths + assert not paths.intersection( + { + "/info", + "/keys", + "/keys/{idBase64Urlsafe}", + "/keysets", + "/mint", + "/melt", + "/checkfees", + "/split", + "/check", + "/restore", + } + ) + + def test_catch_exceptions_maps_cashu_errors_to_json(): app = FastAPI() app.middleware("http")(app_module.catch_exceptions) diff --git a/tests/mint/test_mint_batch.py b/tests/mint/test_mint_batch.py index 0f313494..475efa47 100644 --- a/tests/mint/test_mint_batch.py +++ b/tests/mint/test_mint_batch.py @@ -26,12 +26,6 @@ async def wallet(ledger: Ledger): yield wallet1 -@pytest.fixture(autouse=True) -def setup_settings(): - settings.debug_mint_only_deprecated = False - yield - - @pytest.mark.asyncio async def test_ledger_mint_quote_check(ledger: Ledger, wallet: Wallet): await wallet.load_mint() @@ -159,9 +153,7 @@ async def test_ledger_mint_batch_race(ledger: Ledger, wallet: Wallet): ) results = await asyncio.gather( - ledger.mint_batch(req), - ledger.mint_batch(req), - return_exceptions=True + ledger.mint_batch(req), ledger.mint_batch(req), return_exceptions=True ) successes = [r for r in results if not isinstance(r, Exception)] @@ -196,7 +188,7 @@ async def test_ledger_mint_batch_race_permutations(ledger: Ledger, wallet: Walle outputs=outputs, signatures=[sig1, sig2], ) - + # Different permutation req2 = PostMintBatchRequest( quotes=[mint_quote2.quote, mint_quote1.quote], @@ -206,9 +198,7 @@ async def test_ledger_mint_batch_race_permutations(ledger: Ledger, wallet: Walle ) results = await asyncio.gather( - ledger.mint_batch(req1), - ledger.mint_batch(req2), - return_exceptions=True + ledger.mint_batch(req1), ledger.mint_batch(req2), return_exceptions=True ) successes = [r for r in results if not isinstance(r, Exception)] @@ -216,9 +206,11 @@ async def test_ledger_mint_batch_race_permutations(ledger: Ledger, wallet: Walle assert len(successes) == 1, f"Expected 1 success, got {len(successes)}" assert len(exceptions) == 1, f"Expected 1 exception, got {len(exceptions)}" - + # Ensure the exception is not a timeout or deadlock error but a transaction error - assert any("already pending" in str(e) or "already issued" in str(e) for e in exceptions), f"Unexpected exception: {exceptions}" + assert any( + "already pending" in str(e) or "already issued" in str(e) for e in exceptions + ), f"Unexpected exception: {exceptions}" @pytest.mark.asyncio @@ -230,7 +222,9 @@ async def test_ledger_mint_batch_and_normal_mint_race(ledger: Ledger, wallet: Wa await pay_if_regtest(mint_quote1.request) await pay_if_regtest(mint_quote2.request) - secrets, rs_gen, derivation_paths = await wallet.generate_secrets_from_to(10000, 10002) + secrets, rs_gen, derivation_paths = await wallet.generate_secrets_from_to( + 10000, 10002 + ) outputs, _ = wallet._construct_outputs([64, 32], secrets[:2], rs_gen[:2]) assert mint_quote1.privkey @@ -247,12 +241,16 @@ async def test_ledger_mint_batch_and_normal_mint_race(ledger: Ledger, wallet: Wa ) outputs_normal, _ = wallet._construct_outputs([64], [secrets[2]], [rs_gen[2]]) - sig_normal = nut20.sign_mint_quote(mint_quote1.quote, outputs_normal, mint_quote1.privkey) + sig_normal = nut20.sign_mint_quote( + mint_quote1.quote, outputs_normal, mint_quote1.privkey + ) results = await asyncio.gather( ledger.mint_batch(req_batch), - ledger.mint(outputs=outputs_normal, quote_id=mint_quote1.quote, signature=sig_normal), - return_exceptions=True + ledger.mint( + outputs=outputs_normal, quote_id=mint_quote1.quote, signature=sig_normal + ), + return_exceptions=True, ) successes = [r for r in results if not isinstance(r, Exception)] @@ -301,7 +299,9 @@ def test_mint_batch_and_check_validation(): @pytest.mark.asyncio -async def test_ledger_mint_batch_post_sign_failure_leaves_pending(ledger: Ledger, wallet: Wallet, monkeypatch): +async def test_ledger_mint_batch_post_sign_failure_leaves_pending( + ledger: Ledger, wallet: Wallet, monkeypatch +): from cashu.core.base import MintQuoteState await wallet.load_mint() @@ -355,8 +355,10 @@ async def mock_unset_mint_quotes_pending(quote_ids, state): # Re-minting with the same quotes should fail because they are PENDING (which prevents double-issuance) monkeypatch.undo() # restore original unset_mint_quotes_pending so we can attempt a normal mint, but it should fail on pending check - - secrets2, rs2, derivation_paths2 = await wallet.generate_secrets_from_to(10002, 10003) + + secrets2, rs2, derivation_paths2 = await wallet.generate_secrets_from_to( + 10002, 10003 + ) outputs2, rs2 = wallet._construct_outputs([64, 32], secrets2, rs2) sig1_2 = nut20.sign_mint_quote(mint_quote1.quote, outputs2, mint_quote1.privkey) sig2_2 = nut20.sign_mint_quote(mint_quote2.quote, outputs2, mint_quote2.privkey) @@ -624,9 +626,9 @@ async def test_ledger_mint_batch_atomicity_one_invalid(ledger: Ledger, wallet: W q1_after = await ledger.crud.get_mint_quote( quote_id=mint_quote1.quote, db=ledger.db ) - assert ( - q1_after.state.value == "PAID" - ), f"Quote1 should still be PAID, got {q1_after.state.value}" + assert q1_after.state.value == "PAID", ( + f"Quote1 should still be PAID, got {q1_after.state.value}" + ) secrets2, rs2, derivation_paths2 = await wallet.generate_secrets_from_to( 10002, 10002 diff --git a/tests/mint/test_mint_db.py b/tests/mint/test_mint_db.py index cf459ea6..cdfe617d 100644 --- a/tests/mint/test_mint_db.py +++ b/tests/mint/test_mint_db.py @@ -19,7 +19,6 @@ from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( assert_err, - is_deprecated_api_only, is_github_actions, pay_if_regtest, ) @@ -294,7 +293,8 @@ async def test_db_events_add_client(wallet: Wallet, ledger: Ledger): notification = JSONRPCNotification( method=JSONRPCMethods.SUBSCRIBE.value, params=JSONRPCNotficationParams( - subId="subId", payload=PostMeltQuoteResponse.from_melt_quote(quote_pending).model_dump() + subId="subId", + payload=PostMeltQuoteResponse.from_melt_quote(quote_pending).model_dump(), ).model_dump(), ) @@ -339,7 +339,6 @@ async def test_db_update_mint_quote_state(wallet: Wallet, ledger: Ledger): @pytest.mark.asyncio -@pytest.mark.skipif(is_deprecated_api_only, reason=("Deprecated API")) async def test_db_update_melt_quote_state(wallet: Wallet, ledger: Ledger): melt_quote = await wallet.melt_quote(payment_request) await ledger.db_write._update_melt_quote_state( @@ -497,9 +496,10 @@ async def test_get_melt_quotes_by_checking_id_different_checking_ids(ledger: Led @pytest.mark.asyncio async def test_mint_quote_paid_time_update(wallet: Wallet, ledger: Ledger): import time + # Create a mint quote mint_quote = await wallet.request_mint(128) - + # Check that paid_time is None initially quote = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db) assert quote is not None @@ -509,7 +509,7 @@ async def test_mint_quote_paid_time_update(wallet: Wallet, ledger: Ledger): # Simulate payment await pay_if_regtest(mint_quote.request) - + # Trigger check at mint (this updates the state in DB) _ = await ledger.get_mint_quote(mint_quote.quote) # Check that paid_time is now set diff --git a/tests/mint/test_mint_init.py b/tests/mint/test_mint_init.py index 67a18c86..2b1b0cdc 100644 --- a/tests/mint/test_mint_init.py +++ b/tests/mint/test_mint_init.py @@ -50,8 +50,8 @@ def assert_amt(proofs: List[Proof], expected: int): async def wallet(ledger: Ledger): wallet1 = await Wallet.with_db( url=SERVER_ENDPOINT, - db="test_data/wallet_mint_api_deprecated", - name="wallet_mint_api_deprecated", + db="test_data/wallet_mint_init", + name="wallet_mint_init", ) await wallet1.load_mint() yield wallet1 diff --git a/tests/mint/test_mint_melt.py b/tests/mint/test_mint_melt.py index 97418bc4..4880c929 100644 --- a/tests/mint/test_mint_melt.py +++ b/tests/mint/test_mint_melt.py @@ -25,7 +25,6 @@ from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( get_real_invoice, - is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest, @@ -56,8 +55,8 @@ def assert_amt(proofs: List[Proof], expected: int): async def wallet(ledger: Ledger): wallet1 = await Wallet.with_db( url=SERVER_ENDPOINT, - db="test_data/wallet_mint_api_deprecated", - name="wallet_mint_api_deprecated", + db="test_data/wallet_mint_melt", + name="wallet_mint_melt", ) await wallet1.load_mint() yield wallet1 @@ -99,8 +98,8 @@ async def create_pending_melts( @pytest.mark.asyncio @pytest.mark.skipif( - not is_fake or is_deprecated_api_only, - reason="only fakewallet and non-deprecated api", + not is_fake, + reason="only fakewallet", ) async def test_pending_melt_quote_outputs_registration_regression( wallet, ledger: Ledger @@ -160,8 +159,8 @@ async def test_pending_melt_quote_outputs_registration_regression( @pytest.mark.asyncio @pytest.mark.skipif( - not is_fake or is_deprecated_api_only, - reason="only fakewallet and non-deprecated api", + not is_fake, + reason="only fakewallet", ) async def test_settled_melt_quote_outputs_registration_regression( wallet, ledger: Ledger @@ -218,8 +217,8 @@ async def test_settled_melt_quote_outputs_registration_regression( @pytest.mark.asyncio @pytest.mark.skipif( - not is_fake or is_deprecated_api_only, - reason="only fakewallet and non-deprecated api", + not is_fake, + reason="only fakewallet", ) async def test_melt_quote_reuse_same_outputs(wallet, ledger: Ledger): """Verify that if the same outputs are used in two melt requests, @@ -407,6 +406,7 @@ async def test_melt_lightning_pay_invoice_failed_failed(ledger: Ledger, wallet: except LightningPaymentFailedError: pass + @pytest.mark.asyncio @pytest.mark.skipif(is_regtest, reason="only fake wallet") async def test_melt_lightning_unknown_status_keeps_proofs_pending( @@ -782,7 +782,6 @@ async def test_mint_pay_with_duplicate_checking_id(wallet): @pytest.mark.asyncio -@pytest.mark.skipif(is_deprecated_api_only, reason="Can't run on the deprecated API") async def test_melt_race_condition_fixed(wallet: Wallet, ledger: Ledger): import asyncio @@ -913,8 +912,8 @@ async def test_internal_melt_failure_unsets_pending(ledger: Ledger, wallet: Wall @pytest.mark.asyncio @pytest.mark.skipif( - not is_fake or is_deprecated_api_only, - reason="only fakewallet and non-deprecated api", + not is_fake, + reason="only fakewallet", ) @pytest.mark.parametrize( "fee_paid_sat_offset", diff --git a/tests/mint/test_mint_migrations.py b/tests/mint/test_mint_migrations.py index efb6fed2..258e6b83 100644 --- a/tests/mint/test_mint_migrations.py +++ b/tests/mint/test_mint_migrations.py @@ -1,8 +1,107 @@ +from datetime import datetime, timezone + import pytest +from cashu.core.base import MeltQuote, MeltQuoteState, MintQuote, MintQuoteState from cashu.core.db import Database from cashu.core.migrations import migrate_databases from cashu.mint import migrations as mint_migrations +from cashu.mint.crud import LedgerCrudSqlite + + +@pytest.mark.asyncio +async def test_current_quote_schema_loads_current_fields(tmp_path): + db = Database("mint", str(tmp_path / "current_quote_schema")) + await migrate_databases(db, mint_migrations) + crud = LedgerCrudSqlite() + + async with db.connect() as conn: + mint_columns = { + row["name"] + for row in await conn.fetchall( + f"PRAGMA table_info({db.table_with_schema('mint_quotes')})" + ) + } + melt_columns = { + row["name"] + for row in await conn.fetchall( + f"PRAGMA table_info({db.table_with_schema('melt_quotes')})" + ) + } + + assert {"amount_paid", "amount_issued", "updated_at"} <= mint_columns + assert "proof" in melt_columns + assert "payment_preimage" not in melt_columns + assert "outputs" not in melt_columns + + mint_quote = MintQuote( + quote="mint-quote", + method="bolt11", + request="mint-request", + checking_id="mint-checking-id", + unit="sat", + amount=100, + state=MintQuoteState.paid, + created_time=1000, + paid_time=1200, + amount_paid=40, + amount_issued=20, + updated_at=1500, + ) + await crud.store_mint_quote(quote=mint_quote, db=db) + loaded_mint_quote = await crud.get_mint_quote(quote_id=mint_quote.quote, db=db) + assert loaded_mint_quote is not None + assert loaded_mint_quote.amount_paid == 40 + assert loaded_mint_quote.amount_issued == 20 + assert loaded_mint_quote.updated_at == 1500 + + melt_quote = MeltQuote( + quote="melt-quote", + method="bolt11", + request="melt-request", + checking_id="melt-checking-id", + unit="sat", + amount=100, + fee_reserve=2, + state=MeltQuoteState.paid, + created_time=1000, + paid_time=1200, + fee_paid=1, + payment_preimage="11" * 32, + expiry=2000, + ) + await crud.store_melt_quote(quote=melt_quote, db=db) + loaded_melt_quote = await crud.get_melt_quote(quote_id=melt_quote.quote, db=db) + assert loaded_melt_quote is not None + assert loaded_melt_quote.payment_preimage == "11" * 32 + + await db.engine.dispose() + + +def test_current_mint_quote_postgres_row_loads_accounting_fields(): + row = { + "quote": "mint-quote", + "method": "bolt11", + "request": "mint-request", + "checking_id": "mint-checking-id", + "unit": "sat", + "amount": 100, + "state": MintQuoteState.paid.value, + "created_time": datetime.fromtimestamp(1000, timezone.utc), + "paid_time": datetime.fromtimestamp(1200, timezone.utc), + "issued_time": None, + "last_checked": None, + "pubkey": None, + "amount_paid": 40, + "amount_issued": 20, + "updated_at": datetime.fromtimestamp(1500, timezone.utc), + } + + quote = MintQuote.from_row(row) # type: ignore[arg-type] + + assert quote.amount_paid == 40 + assert quote.amount_issued == 20 + assert quote.updated_at == 1500 @pytest.mark.asyncio @@ -34,14 +133,14 @@ async def test_m029_witness_cleanup(): # Insert into proofs_used await conn.execute( f""" - INSERT INTO {db.table_with_schema('proofs_used')} (amount, id, c, secret, y, witness, created, melt_quote) + INSERT INTO {db.table_with_schema("proofs_used")} (amount, id, c, secret, y, witness, created, melt_quote) VALUES (1, 'kid', 'c_used_long', 's_used_long', 'y_used_long', :w, {db.timestamp_now}, NULL) """, {"w": long_witness}, ) await conn.execute( f""" - INSERT INTO {db.table_with_schema('proofs_used')} (amount, id, c, secret, y, witness, created, melt_quote) + INSERT INTO {db.table_with_schema("proofs_used")} (amount, id, c, secret, y, witness, created, melt_quote) VALUES (1, 'kid', 'c_used_short', 's_used_short', 'y_used_short', :w, {db.timestamp_now}, NULL) """, {"w": short_witness}, @@ -50,14 +149,14 @@ async def test_m029_witness_cleanup(): # Insert into proofs_pending await conn.execute( f""" - INSERT INTO {db.table_with_schema('proofs_pending')} (amount, id, c, secret, y, witness, created, melt_quote) + INSERT INTO {db.table_with_schema("proofs_pending")} (amount, id, c, secret, y, witness, created, melt_quote) VALUES (1, 'kid', 'c_pend_long', 's_pend_long', 'y_pend_long', :w, {db.timestamp_now}, NULL) """, {"w": long_witness}, ) await conn.execute( f""" - INSERT INTO {db.table_with_schema('proofs_pending')} (amount, id, c, secret, y, witness, created, melt_quote) + INSERT INTO {db.table_with_schema("proofs_pending")} (amount, id, c, secret, y, witness, created, melt_quote) VALUES (1, 'kid', 'c_pend_short', 's_pend_short', 'y_pend_short', :w, {db.timestamp_now}, NULL) """, {"w": short_witness}, diff --git a/tests/mint/test_mint_operations.py b/tests/mint/test_mint_operations.py index 1c407191..d13fb523 100644 --- a/tests/mint/test_mint_operations.py +++ b/tests/mint/test_mint_operations.py @@ -58,27 +58,26 @@ async def test_melt_internal(wallet1: Wallet, ledger: Ledger): assert melt_quote.amount == 64 assert melt_quote.fee_reserve == 0 - if not settings.debug_mint_only_deprecated: - melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote) - assert melt_quote_response_pre_payment - assert not melt_quote_response_pre_payment.state == MeltQuoteState.paid, ( - "melt quote should not be paid" - ) - assert melt_quote_response_pre_payment.amount == 64 + melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote) + assert melt_quote_response_pre_payment + assert ( + not melt_quote_response_pre_payment.state == MeltQuoteState.paid + ), "melt quote should not be paid" + assert melt_quote_response_pre_payment.amount == 64 melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_pre_payment.state != MeltQuoteState.paid, ( - "melt quote should not be paid" - ) + assert ( + melt_quote_pre_payment.state != MeltQuoteState.paid + ), "melt quote should not be paid" assert melt_quote_pre_payment.state == MeltQuoteState.unpaid keep_proofs, send_proofs = await wallet1.swap_to_send(wallet1.proofs, 64) await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_post_payment.state == MeltQuoteState.paid, ( - "melt quote should be paid" - ) + assert ( + melt_quote_post_payment.state == MeltQuoteState.paid + ), "melt quote should be paid" assert melt_quote_post_payment.state == MeltQuoteState.paid @@ -104,27 +103,26 @@ async def test_melt_external(wallet1: Wallet, ledger: Ledger): PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") ) - if not settings.debug_mint_only_deprecated: - melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote) - assert melt_quote_response_pre_payment - assert melt_quote_response_pre_payment.state == MeltQuoteState.unpaid, ( - "melt quote should not be paid" - ) - assert melt_quote_response_pre_payment.amount == melt_quote.amount + melt_quote_response_pre_payment = await wallet1.get_melt_quote(melt_quote.quote) + assert melt_quote_response_pre_payment + assert ( + melt_quote_response_pre_payment.state == MeltQuoteState.unpaid + ), "melt quote should not be paid" + assert melt_quote_response_pre_payment.amount == melt_quote.amount melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_pre_payment.state != MeltQuoteState.paid, ( - "melt quote should not be paid" - ) + assert ( + melt_quote_pre_payment.state != MeltQuoteState.paid + ), "melt quote should not be paid" assert melt_quote_pre_payment.state == MeltQuoteState.unpaid assert melt_quote.state != MeltQuoteState.paid, "melt quote should not be paid" await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_post_payment.state == MeltQuoteState.paid, ( - "melt quote should be paid" - ) + assert ( + melt_quote_post_payment.state == MeltQuoteState.paid + ), "melt quote should be paid" assert melt_quote_post_payment.state == MeltQuoteState.paid @@ -137,9 +135,8 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger): assert mint_quote.state == MintQuoteState.paid, "mint quote should be paid" - if not settings.debug_mint_only_deprecated: - mint_quote = await wallet1.get_mint_quote(mint_quote.quote) - assert mint_quote.state == MintQuoteState.paid, "mint quote should be paid" + mint_quote = await wallet1.get_mint_quote(mint_quote.quote) + assert mint_quote.state == MintQuoteState.paid, "mint quote should be paid" output_amounts = [128] secrets, rs, derivation_paths = await wallet1.generate_n_secrets( @@ -171,9 +168,8 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger): assert mint_quote.state != MintQuoteState.paid, "mint quote already paid" assert mint_quote.state == MintQuoteState.unpaid - if not settings.debug_mint_only_deprecated: - mint_quote = await wallet1.get_mint_quote(quote.quote) - assert mint_quote.state != MintQuoteState.paid, "mint quote should not be paid" + mint_quote = await wallet1.get_mint_quote(quote.quote) + assert mint_quote.state != MintQuoteState.paid, "mint quote should not be paid" await assert_err( wallet1.mint(128, quote_id=quote.quote), diff --git a/tests/test_mint_rpc_cli.py b/tests/test_mint_rpc_cli.py index 17b083b4..a9eea5eb 100644 --- a/tests/test_mint_rpc_cli.py +++ b/tests/test_mint_rpc_cli.py @@ -7,7 +7,7 @@ from cashu.mint.management_rpc.cli.cli import cli from cashu.wallet.wallet import Wallet -from .helpers import is_deprecated_api_only, is_fake +from .helpers import is_fake payment_request = ( "lnbc10u1pjap7phpp50s9lzr3477j0tvacpfy2ucrs4q0q6cvn232ex7nt2zqxxxj8gxrsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss" @@ -115,10 +115,6 @@ async def test_update_mint_quote(cli_prefix): assert "Successfully updated!" in result.output @pytest.mark.asyncio -@pytest.mark.skipif( - is_deprecated_api_only, - reason=("Deprecated API"), -) async def test_update_melt_quote(cli_prefix): wallet = await init_wallet() melt_quote = await wallet.melt_quote("lnbc1u1p5qefdgsp5xj5cl559ks226f3vf3d7x2ev2qadplmkswp4649h755cfekdufsspp5sxenacdev78ssuwn5vehycs7ch2ds23hhzytut4ncm27gywtv6rqdqqcqpjrzjqdgp5ar48c8k4cns58jw9lamcdlh57trvrn9psgjrsvwz94j9tqsvrqsvcqqvqsqqqqqqqlgqqqzwyqq2q9qxpqysgqzg8e75zkcxazmd0wqmre6xgkumt7sl4ftsw0q4c6zvz8hn6zjxwz9fmdmwpupw7tw79f7gmukyeeh8vusvt03pgwfud9shj849rvrnqpgcpusw") @@ -142,10 +138,6 @@ async def test_get_mint_quote(cli_prefix): assert "mint quote:" in result.output @pytest.mark.asyncio -@pytest.mark.skipif( - is_deprecated_api_only, - reason=("Deprecated API"), -) async def test_get_melt_quote(cli_prefix): wallet = await init_wallet() melt_quote = await wallet.melt_quote("lnbc1u1p5qefd7sp55l6kmcrnqz5rejy4lghmgf9de0ucmmn2s3lvkvtkrr0qkwk5r0espp5da4x63rspz5rcfretdh6573c6qlpnzpxc8yq26cyqjc4sk0srfwsdqqcqpjrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur2z3sqqrrgqqyqqqqqqqqqqfcsqjq9qxpqysgq4l5rfjd4h84w7prmtgzjvq79ddy266svuz0d7dg44jmnwjpxg0zxef6hn4j8nzfp4c67qjpe0c9aw63ghu7rtcdg6n4zka9hym69euqq8w5wmj") @@ -170,10 +162,6 @@ async def test_get_quote_ttl_mint_quote(cli_prefix): assert "Error:" in result.output @pytest.mark.asyncio -@pytest.mark.skipif( - is_deprecated_api_only, - reason=("Deprecated API"), -) async def test_get_quote_ttl_melt_quote(cli_prefix): """Melt quotes persist expiry; GetQuoteTtl should return the expiry timestamp.""" wallet = await init_wallet() diff --git a/tests/test_mint_watchdog.py b/tests/test_mint_watchdog.py index cf0753f9..ae081193 100644 --- a/tests/test_mint_watchdog.py +++ b/tests/test_mint_watchdog.py @@ -77,15 +77,16 @@ async def test_balance_update_on_test_melt_internal(wallet: Wallet, ledger: Ledg PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") ) - if not settings.debug_mint_only_deprecated: - melt_quote_response_pre_payment = await wallet.get_melt_quote(melt_quote.quote) - assert ( - not melt_quote_response_pre_payment.state == MeltQuoteState.paid.value - ), "melt quote should not be paid" - assert melt_quote_response_pre_payment.amount == payment_amount + melt_quote_response_pre_payment = await wallet.get_melt_quote(melt_quote.quote) + assert ( + not melt_quote_response_pre_payment.state == MeltQuoteState.paid.value + ), "melt quote should not be paid" + assert melt_quote_response_pre_payment.amount == payment_amount melt_quote_pre_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_pre_payment.state != MeltQuoteState.paid, "melt quote should not be paid" + assert ( + melt_quote_pre_payment.state != MeltQuoteState.paid + ), "melt quote should not be paid" assert melt_quote_pre_payment.state == MeltQuoteState.unpaid _, send_proofs = await wallet.swap_to_send(wallet.proofs, payment_amount) @@ -94,7 +95,9 @@ async def test_balance_update_on_test_melt_internal(wallet: Wallet, ledger: Ledg assert wallet.balance == 64 melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_post_payment.state == MeltQuoteState.paid, "melt quote should be paid" + assert ( + melt_quote_post_payment.state == MeltQuoteState.paid + ), "melt quote should be paid" balance_after, fees_paid_after = await ledger.get_unit_balance_and_fees( Unit.sat, ledger.db @@ -140,12 +143,11 @@ async def test_balance_update_on_melt_external(wallet: Wallet, ledger: Ledger): PostMeltQuoteRequest(request=invoice_payment_request, unit="sat") ) - if not settings.debug_mint_only_deprecated: - melt_quote_response_pre_payment = await wallet.get_melt_quote(melt_quote.quote) - assert ( - melt_quote_response_pre_payment.state == MeltQuoteState.unpaid.value - ), "melt quote should not be paid" - assert melt_quote_response_pre_payment.amount == melt_quote.amount + melt_quote_response_pre_payment = await wallet.get_melt_quote(melt_quote.quote) + assert ( + melt_quote_response_pre_payment.state == MeltQuoteState.unpaid.value + ), "melt quote should not be paid" + assert melt_quote_response_pre_payment.amount == melt_quote.amount melt_quote_resp = await ledger.melt(proofs=send_proofs, quote=melt_quote.quote) fees_paid = melt_quote.fee_reserve - ( @@ -153,7 +155,9 @@ async def test_balance_update_on_melt_external(wallet: Wallet, ledger: Ledger): ) melt_quote_post_payment = await ledger.get_melt_quote(melt_quote.quote) - assert melt_quote_post_payment.state == MeltQuoteState.paid, "melt quote should be paid" + assert ( + melt_quote_post_payment.state == MeltQuoteState.paid + ), "melt quote should be paid" balance_after, fees_paid_after = await ledger.get_unit_balance_and_fees( Unit.sat, ledger.db diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 5c27200f..df4f7ef6 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -29,7 +29,6 @@ from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( get_real_invoice, - is_deprecated_api_only, is_fake, is_github_actions, is_regtest, @@ -185,11 +184,11 @@ async def test_request_mint(wallet1: Wallet): @pytest.mark.asyncio async def test_mint(wallet1: Wallet): mint_quote = await wallet1.request_mint(64) + mint_request = mint_quote.request await pay_if_regtest(mint_quote.request) - if not settings.debug_mint_only_deprecated: - mint_quote = await wallet1.get_mint_quote(mint_quote.quote) - assert mint_quote.request == mint_quote.request - assert mint_quote.state == MintQuoteState.paid + mint_quote = await wallet1.get_mint_quote(mint_quote.quote) + assert mint_quote.request == mint_request + assert mint_quote.state == MintQuoteState.paid expected_proof_amounts = wallet1.split_wallet_state(64) await wallet1.mint(64, quote_id=mint_quote.quote) @@ -325,10 +324,9 @@ async def test_melt(wallet1: Wallet): assert total_amount == 64 assert quote.fee_reserve == 0 - if not settings.debug_mint_only_deprecated: - quote_resp = await wallet1.get_melt_quote(quote.quote) - assert quote_resp - assert quote_resp.amount == quote.amount + quote_resp = await wallet1.get_melt_quote(quote.quote) + assert quote_resp + assert quote_resp.amount == quote.amount _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount) @@ -354,15 +352,14 @@ async def test_melt(wallet1: Wallet): assert melt_quote_db, "No melt quote in db" # compare melt quote from API against db - if not settings.debug_mint_only_deprecated: - melt_quote_api_resp = await wallet1.get_melt_quote(melt_quote_db.quote) - assert melt_quote_api_resp, "No melt quote from API" - assert melt_quote_api_resp.quote == melt_quote_db.quote, "Wrong quote ID" - assert melt_quote_api_resp.amount == melt_quote_db.amount, "Wrong amount" - assert melt_quote_api_resp.fee_reserve == melt_quote_db.fee_reserve, "Wrong fee" - assert melt_quote_api_resp.request == melt_quote_db.request, "Wrong request" - assert melt_quote_api_resp.state == melt_quote_db.state, "Wrong state" - assert melt_quote_api_resp.unit == melt_quote_db.unit, "Wrong unit" + melt_quote_api_resp = await wallet1.get_melt_quote(melt_quote_db.quote) + assert melt_quote_api_resp, "No melt quote from API" + assert melt_quote_api_resp.quote == melt_quote_db.quote, "Wrong quote ID" + assert melt_quote_api_resp.amount == melt_quote_db.amount, "Wrong amount" + assert melt_quote_api_resp.fee_reserve == melt_quote_db.fee_reserve, "Wrong fee" + assert melt_quote_api_resp.request == melt_quote_db.request, "Wrong request" + assert melt_quote_api_resp.state == melt_quote_db.state, "Wrong state" + assert melt_quote_api_resp.unit == melt_quote_db.unit, "Wrong unit" proofs_used = await get_proofs( db=wallet1.db, melt_id=melt_quote_db.quote, table="proofs_used" @@ -377,7 +374,6 @@ async def test_melt(wallet1: Wallet): @pytest.mark.asyncio -@pytest.mark.skipif(is_deprecated_api_only, reason="Deprecated API only") async def test_get_melt_quote_state(wallet1: Wallet): mint_quote = await wallet1.request_mint(128) await pay_if_regtest(mint_quote.request) @@ -404,8 +400,6 @@ async def test_get_melt_quote_state(wallet1: Wallet): melt_quote_wallet = MeltQuote.from_resp_wallet( melt_response, mint="test", - unit=quote.unit or "sat", - request=quote.request or invoice_payment_request, ) # compare melt quote from API against db diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 3739513b..1d76bc75 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -13,7 +13,6 @@ from cashu.wallet.wallet import Wallet from tests.helpers import ( get_real_invoice, - is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest, @@ -59,6 +58,7 @@ async def reset_invoices(wallet: Wallet): async def init_wallet(): settings.debug = False + assert settings.mint_url wallet = await Wallet.with_db( url=settings.mint_url, db="test_data/test_cli_wallet", @@ -137,9 +137,6 @@ def test_pay_invoice_regtest(mint, cli_prefix): @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") def test_invoice(mint, cli_prefix): - if settings.debug_mint_only_deprecated: - pytest.skip("only works with v1 API") - runner = CliRunner() result = runner.invoke( cli, @@ -153,9 +150,6 @@ def test_invoice(mint, cli_prefix): @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") def test_invoice_verbose(mint, cli_prefix): - if settings.debug_mint_only_deprecated: - pytest.skip("only works with v1 API") - runner = CliRunner() result = runner.invoke( cli, @@ -191,7 +185,6 @@ def test_invoice_return_immediately(mint, cli_prefix): assert result.exit_code == 0 -@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") def test_invoice_with_memo(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -516,7 +509,9 @@ def test_send_too_much(mint, cli_prefix): assert "Balance too low" in str(result.exception) -@pytest.mark.skip(reason="Test uses hardcoded token that doesn't match current mint keysets. Should be rewritten to generate token dynamically.") +@pytest.mark.skip( + reason="Test uses hardcoded token that doesn't match current mint keysets. Should be rewritten to generate token dynamically." +) def test_receive_tokenv3(mint, cli_prefix): runner = CliRunner() token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOWExZjI5MzI1M2U0MWUiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICI0NzlkY2E0MzUzNzU4MTM4N2Q1ODllMDU1MGY0Y2Q2MjFmNjE0MDM1MGY5M2Q4ZmI1OTA2YjJlMGRiNmRjYmI3IiwgIkMiOiAiMDM1MGQ0ZmI0YzdiYTMzNDRjMWRjYWU1ZDExZjNlNTIzZGVkOThmNGY4ODdkNTQwZmYyMDRmNmVlOWJjMjkyZjQ1In0sIHsiaWQiOiAiMDA5YTFmMjkzMjUzZTQxZSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogIjZjNjAzNDgwOGQyNDY5N2IyN2YxZTEyMDllNjdjNjVjNmE2MmM2Zjc3NGI4NWVjMGQ5Y2Y3MjE0M2U0NWZmMDEiLCAiQyI6ICIwMjZkNDlhYTE0MmFlNjM1NWViZTJjZGQzYjFhOTdmMjE1MDk2NTlkMDE3YWU0N2FjNDY3OGE4NWVkY2E4MGMxYmQifV0sICJtaW50IjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyJ9XX0=" # noqa @@ -715,7 +710,9 @@ def test_send_with_lock_and_timelock(mint, cli_prefix): token_str = result.output.split("\n")[0] assert "cashuB" in token_str, "output does not have a token" token = TokenV4.deserialize(token_str).to_tokenv3() - secret = P2PKSecret.deserialize(token.token[0].proofs[0].secret) + secret = P2PKSecret.from_secret( + P2PKSecret.deserialize(token.token[0].proofs[0].secret) + ) assert secret.locktime is not None assert before + 5 <= secret.locktime <= after + 5 @@ -742,13 +739,13 @@ def test_send_with_lock_uses_locktime_delta_seconds_by_default(mint, cli_prefix) ) after = int(time.time()) assert result.exception is None - print( - "test_send_with_lock_uses_locktime_delta_seconds_by_default", result.output - ) + print("test_send_with_lock_uses_locktime_delta_seconds_by_default", result.output) token_str = result.output.split("\n")[0] assert "cashuB" in token_str, "output does not have a token" token = TokenV4.deserialize(token_str).to_tokenv3() - secret = P2PKSecret.deserialize(token.token[0].proofs[0].secret) + secret = P2PKSecret.from_secret( + P2PKSecret.deserialize(token.token[0].proofs[0].secret) + ) assert secret.locktime is not None assert ( before + settings.locktime_delta_seconds @@ -775,7 +772,9 @@ def mint_tokens(runner, cli_prefix, amount: str): def test_proofs_basic(cli_prefix): """Test basic proofs command functionality""" - runner = CliRunner(mix_stderr=False) # Separate stdout/stderr, as we want to verify only stdout + runner = CliRunner( + mix_stderr=False + ) # Separate stdout/stderr, as we want to verify only stdout # First create some tokens like other tests do mint_tokens(runner, cli_prefix, "64") @@ -794,6 +793,7 @@ def test_proofs_basic(cli_prefix): # Should be valid JSON import json + proofs = json.loads(json_output) assert isinstance(proofs, list) assert len(proofs) > 0, "Should have proofs after minting tokens" @@ -801,7 +801,7 @@ def test_proofs_basic(cli_prefix): # Each proof should have the expected fields for proof in proofs: - assert sorted(proof.keys()) == ['C', 'amount', 'dleq', 'id', 'secret'] + assert sorted(proof.keys()) == ["C", "amount", "dleq", "id", "secret"] def test_proofs_json_structure(cli_prefix): @@ -872,9 +872,9 @@ def test_proofs_with_no_dleq_flag(cli_prefix): assert isinstance(proof["amount"], int), "'amount' should be integer" assert isinstance(proof["secret"], str), "'secret' should be string" assert isinstance(proof["C"], str), "'C' should be string" - assert ( - "dleq" not in proof - ), "proof should not contain 'dleq' field with --no-dleq" + assert "dleq" not in proof, ( + "proof should not contain 'dleq' field with --no-dleq" + ) def test_proofs_with_keyset_filter(cli_prefix): @@ -912,9 +912,9 @@ def test_proofs_with_keyset_filter(cli_prefix): # All filtered proofs should have the same keyset ID for proof in filtered_proofs: - assert ( - proof["id"] == test_keyset - ), f"proof has wrong keyset ID: {proof['id']} != {test_keyset}" + assert proof["id"] == test_keyset, ( + f"proof has wrong keyset ID: {proof['id']} != {test_keyset}" + ) # Filter with a non-existent keyset, to make sure nothing is returned import secrets @@ -963,9 +963,9 @@ def test_proofs_with_all_flag(cli_prefix): all_proofs = json.loads(result_all.stdout.strip()) # All proofs should include at least the same number as available proofs - assert len(all_proofs) > len( - available_proofs - ), "--all should have more proofs, as it includes the reserved proofs" + assert len(all_proofs) > len(available_proofs), ( + "--all should have more proofs, as it includes the reserved proofs" + ) print(f"Available proofs: {len(available_proofs)}, All proofs: {len(all_proofs)}") @@ -973,6 +973,6 @@ def test_proofs_with_all_flag(cli_prefix): available_secrets = {proof["secret"] for proof in available_proofs} all_secrets = {proof["secret"] for proof in all_proofs} - assert available_secrets.issubset( - all_secrets - ), "all available proofs should be included in --all" + assert available_secrets.issubset(all_secrets), ( + "all available proofs should be included in --all" + ) diff --git a/tests/wallet/test_wallet_crud_unit.py b/tests/wallet/test_wallet_crud_unit.py index 5bf2c9ee..94c4cd3c 100644 --- a/tests/wallet/test_wallet_crud_unit.py +++ b/tests/wallet/test_wallet_crud_unit.py @@ -388,6 +388,7 @@ def test_mint_quote_stale_check_handles_missing_local_accounting_fields(): request="req-migrated", amount=100, unit="sat", + method="bolt11", state=MintQuoteState.unpaid.value, amount_paid=0, amount_issued=0, @@ -411,6 +412,7 @@ def test_mint_quote_from_resp_wallet_prioritizes_pending(): request="req-pending-test", amount=100, unit="sat", + method="bolt11", state="PENDING", amount_paid=100, amount_issued=0, @@ -420,8 +422,6 @@ def test_mint_quote_from_resp_wallet_prioritizes_pending(): quote = MintQuote.from_resp_wallet( mint_response, mint="https://mint.test", - amount=100, - unit="sat", ) assert quote.state == MintQuoteState.pending diff --git a/tests/wallet/test_wallet_lightning.py b/tests/wallet/test_wallet_lightning.py index b8b11476..d41df2e5 100644 --- a/tests/wallet/test_wallet_lightning.py +++ b/tests/wallet/test_wallet_lightning.py @@ -10,7 +10,6 @@ from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( get_real_invoice, - is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest, @@ -66,7 +65,6 @@ async def test_create_invoice(wallet: LightningWallet): @pytest.mark.asyncio -@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") async def test_create_invoice_with_description(wallet: LightningWallet): invoice = await wallet.create_invoice(64, "test description") assert invoice.payment_request diff --git a/tests/wallet/test_wallet_p2pk.py b/tests/wallet/test_wallet_p2pk.py index 4cfdf08a..5484f3ea 100644 --- a/tests/wallet/test_wallet_p2pk.py +++ b/tests/wallet/test_wallet_p2pk.py @@ -21,7 +21,7 @@ from cashu.wallet.wallet import Wallet as Wallet1 from cashu.wallet.wallet import Wallet as Wallet2 from tests.conftest import SERVER_ENDPOINT -from tests.helpers import is_deprecated_api_only, pay_if_regtest +from tests.helpers import pay_if_regtest async def assert_err(f, msg): @@ -86,12 +86,11 @@ async def test_p2pk(wallet1: Wallet, wallet2: Wallet): proof_states = await wallet2.check_proof_state(send_proofs) assert all([p.spent for p in proof_states.states]) - if not is_deprecated_api_only: - for state in proof_states.states: - assert state.witness is not None - witness_obj = json.loads(state.witness) - assert len(witness_obj["signatures"]) == 1 - assert len(witness_obj["signatures"][0]) == 128 + for state in proof_states.states: + assert state.witness is not None + witness_obj = json.loads(state.witness) + assert len(witness_obj["signatures"]) == 1 + assert len(witness_obj["signatures"][0]) == 128 @pytest.mark.asyncio diff --git a/tests/wallet/test_wallet_v1_api.py b/tests/wallet/test_wallet_v1_api.py index 8cc5c5dd..2c2bf492 100644 --- a/tests/wallet/test_wallet_v1_api.py +++ b/tests/wallet/test_wallet_v1_api.py @@ -3,10 +3,16 @@ import httpx import pytest +from pydantic import ValidationError from cashu.core.base import BlindedMessage, MeltQuoteState, Proof, Unit from cashu.core.crypto.secp import PrivateKey from cashu.core.db import Database +from cashu.core.models import ( + GetInfoResponse, + PostMeltQuoteResponse, + PostMintQuoteResponse, +) from cashu.core.settings import settings from cashu.wallet.v1_api import LedgerAPI @@ -258,11 +264,60 @@ async def fake_request(self, method, path, **kwargs): assert method == "GET" assert path == "/v1/info" assert kwargs["noprefix"] is True - return _response(200, {"name": "MintName", "version": "1.0.0"}) + return _response( + 200, + { + "name": "MintName", + "version": "1.0.0", + "contact": [{"method": "email", "info": "mint@example.com"}], + }, + ) monkeypatch.setattr(api, "_request", MethodType(fake_request, api)) mint_info = await api._get_info() assert mint_info.name == "MintName" + assert mint_info.contact + assert mint_info.contact[0].method == "email" + assert mint_info.contact[0].info == "mint@example.com" + + +def test_get_info_rejects_deprecated_contact_shape(): + with pytest.raises(ValidationError): + GetInfoResponse.model_validate({"contact": [["email", "mint@example.com"]]}) + + +@pytest.mark.parametrize("missing", ["amount", "unit", "method", "state"]) +def test_mint_quote_response_requires_current_fields(missing: str): + response = { + "quote": "q-1", + "request": "lnbc1", + "amount": 1, + "unit": "sat", + "method": "bolt11", + "state": "UNPAID", + } + del response[missing] + + with pytest.raises(ValidationError): + PostMintQuoteResponse.model_validate(response) + + +@pytest.mark.parametrize("missing", ["unit", "method", "request", "state"]) +def test_melt_quote_response_requires_current_fields(missing: str): + response = { + "quote": "q-1", + "amount": 1, + "unit": "sat", + "method": "bolt11", + "request": "lnbc1", + "fee_reserve": 1, + "state": "UNPAID", + "expiry": None, + } + del response[missing] + + with pytest.raises(ValidationError): + PostMeltQuoteResponse.model_validate(response) @pytest.mark.asyncio @@ -362,6 +417,7 @@ async def fake_request(self, method, path, **kwargs): "request": "lnbc1", "amount": 21, "unit": "sat", + "method": "bolt11", "state": "UNPAID", "expiry": 123, }, @@ -476,6 +532,34 @@ async def fake_request(self, method, path, **kwargs): mint_payload = [c for c in requests if c[1] == "mint/bolt11"][0][2]["json"] assert set(mint_payload.keys()) == {"quote", "outputs", "signature"} assert set(mint_payload["outputs"][0].keys()) == {"id", "amount", "B_"} + checkstate_payload = [c for c in requests if c[1] == "checkstate"][0][2]["json"] + assert checkstate_payload == {"Ys": [proof.Y]} + + +@pytest.mark.asyncio +async def test_check_proof_state_does_not_retry_with_secrets( + monkeypatch, api: LedgerAPI +): + proof = Proof( + id="kid", amount=1, C=PrivateKey().public_key.format().hex(), secret="s1" + ) + cast(Any, api).keysets = {"kid": object()} + requests = [] + + async def fake_request(self, method, path, **kwargs): + requests.append((method, path, kwargs)) + return _response(422, {"detail": "invalid current request"}) + + monkeypatch.setattr( + "cashu.wallet.v1_api.httpx.AsyncClient", lambda **kwargs: object() + ) + monkeypatch.setattr(api, "_request", MethodType(fake_request, api)) + + with pytest.raises(Exception, match="invalid current request"): + await api.check_proof_state([proof]) + + assert len(requests) == 1 + assert requests[0][2]["json"] == {"Ys": [proof.Y]} @pytest.mark.asyncio @@ -500,6 +584,7 @@ async def fake_request(self, method, path, **kwargs): "quote": "m-1", "amount": 1, "unit": "sat", + "method": "bolt11", "request": "lnbc1", "fee_reserve": 1, "state": "UNPAID", @@ -513,6 +598,7 @@ async def fake_request(self, method, path, **kwargs): "quote": "m-1", "amount": 1, "unit": "sat", + "method": "bolt11", "request": "lnbc1", "fee_reserve": 1, "state": "UNPAID", @@ -526,6 +612,7 @@ async def fake_request(self, method, path, **kwargs): "quote": "m-1", "amount": 1, "unit": "sat", + "method": "bolt11", "request": "lnbc1", "fee_reserve": 1, "state": "PAID", @@ -563,6 +650,33 @@ async def fake_request(self, method, path, **kwargs): } +@pytest.mark.asyncio +async def test_melt_rejects_deprecated_response(monkeypatch, api: LedgerAPI): + proof = Proof( + id="kid", amount=1, C=PrivateKey().public_key.format().hex(), secret="s2" + ) + cast(Any, api).keysets = {"kid": object()} + + async def fake_request(self, method, path, **kwargs): + assert path == "melt/bolt11" + return _response( + 200, + { + "paid": True, + "preimage": "11" * 32, + "change": [], + }, + ) + + monkeypatch.setattr( + "cashu.wallet.v1_api.httpx.AsyncClient", lambda **kwargs: object() + ) + monkeypatch.setattr(api, "_request", MethodType(fake_request, api)) + + with pytest.raises(ValidationError): + await api.melt("m-1", [proof], None) + + @pytest.mark.asyncio async def test_get_keysets_and_get_keys_filters_unsupported_versions(monkeypatch, api: LedgerAPI): async def fake_request(self, method, path, **kwargs):