diff --git a/README.md b/README.md index d197cd1..e2ed341 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ cashu info Returns: ```bash -Version: 0.12.2 +Version: 0.12.3 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/base.py b/cashu/core/base.py index 4945b81..c068056 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -253,6 +253,10 @@ class CheckSpendableRequest(BaseModel): class CheckSpendableResponse(BaseModel): spendable: List[bool] + pending: Optional[ + List[bool] + ] = None # TODO: Uncomment when all mints are updated to 0.12.3 and support /check + # with pending tokens (kept for backwards compatibility of new wallets with old mints) class CheckFeesRequest(BaseModel): diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 80583b3..3dd79fc 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field env = Env() -VERSION = "0.12.2" +VERSION = "0.12.3" def find_env_file(): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index fe5d903..f5c6ffd 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -2,7 +2,7 @@ import asyncio import json import math import time -from typing import Dict, List, Literal, Optional, Set, Union +from typing import Dict, List, Literal, Optional, Set, Tuple, Union from loguru import logger @@ -186,6 +186,15 @@ class Ledger: """Checks whether the proof was already spent.""" return not proof.secret in self.proofs_used + async def _check_pending(self, proofs: List[Proof]): + """Checks whether the proof is still pending.""" + proofs_pending = await self.crud.get_proofs_pending(db=self.db) + pending_secrets = [pp.secret for pp in proofs_pending] + pending_states = [ + True if p.secret in pending_secrets else False for p in proofs + ] + return pending_states + def _verify_secret_criteria(self, proof: Proof) -> Literal[True]: """Verifies that a secret is present and is not too long (DOS prevention).""" if proof.secret is None or proof.secret == "": @@ -815,12 +824,14 @@ class Ledger: return status, preimage, return_promises - async def check_spendable(self, proofs: List[Proof]): - """Checks if provided proofs are valid and have not been spent yet. - Used by wallets to check if their proofs have been redeemed by a receiver. + async def check_proof_state( + self, proofs: List[Proof] + ) -> Tuple[List[bool], List[bool]]: + """Checks if provided proofs are spend or are pending. + Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. - Returns a list in the same order as the provided proofs. Wallet must match the list - to the proofs they have provided in order to figure out which proof is still spendable + Returns two lists that are in the same order as the provided proofs. Wallet must match the list + to the proofs they have provided in order to figure out which proof is spendable or pending and which isn't. Args: @@ -828,8 +839,11 @@ class Ledger: Returns: List[bool]: List of which proof is still spendable (True if still spendable, else False) + List[bool]: List of which proof are pending (True if pending, else False) """ - return [self._check_spendable(p) for p in proofs] + spendable = [self._check_spendable(p) for p in proofs] + pending = await self._check_pending(proofs) + return spendable, pending async def check_fees(self, pr: str): """Returns the fee reserve (in sat) that a wallet must add to its proofs diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 728000d..d8bde40 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -173,17 +173,18 @@ async def melt(payload: PostMeltRequest) -> Union[CashuError, GetMeltResponse]: @router.post( "/check", - name="Check spendable", - summary="Check whether a proof has already been spent", + name="Check proof state", + summary="Check whether a proof is spent already or is pending in a transaction", ) async def check_spendable( payload: CheckSpendableRequest, ) -> CheckSpendableResponse: """Check whether a secret has been spent already or not.""" logger.trace(f"> POST /check: {payload}") - spendableList = await ledger.check_spendable(payload.proofs) - logger.trace(f"< POST /check: {spendableList}") - return CheckSpendableResponse(spendable=spendableList) + spendableList, pendingList = await ledger.check_proof_state(payload.proofs) + logger.trace(f"< POST /check : {spendableList}") + logger.trace(f"< POST /check : {pendingList}") + return CheckSpendableResponse(spendable=spendableList, pending=pendingList) @router.post( diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 1314a85..d0f7bf4 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -415,6 +415,7 @@ async def receive_cli( @coro async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str): wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_proofs() if not delete: await wallet.load_mint() if not (all or token or force or delete) or (token and all): diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 93065dc..35a1caf 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -498,13 +498,13 @@ class LedgerAPI: return frst_proofs, scnd_proofs @async_set_requests - async def check_spendable(self, proofs: List[Proof]): + async def check_proof_state(self, proofs: List[Proof]): """ Cheks whether the secrets in proofs are already spent or not and returns a list of booleans. """ payload = CheckSpendableRequest(proofs=proofs) - def _check_spendable_include_fields(proofs): + def _check_proof_state_include_fields(proofs): """strips away fields from the model that aren't necessary for the /split""" return { "proofs": {i: {"secret"} for i in range(len(proofs))}, @@ -512,13 +512,13 @@ class LedgerAPI: resp = self.s.post( self.url + "/check", - json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore + json=payload.dict(include=_check_proof_state_include_fields(proofs)), # type: ignore ) resp.raise_for_status() return_dict = resp.json() self.raise_on_error(return_dict) - spendable = CheckSpendableResponse.parse_obj(return_dict) - return spendable + states = CheckSpendableResponse.parse_obj(return_dict) + return states @async_set_requests async def check_fees(self, payment_request: str): @@ -769,8 +769,8 @@ class Wallet(LedgerAPI): raise Exception("could not pay invoice.") return status.paid - async def check_spendable(self, proofs): - return await super().check_spendable(proofs) + async def check_proof_state(self, proofs): + return await super().check_proof_state(proofs) # ---------- TOKEN MECHANIS ---------- @@ -971,8 +971,8 @@ class Wallet(LedgerAPI): """ invalidated_proofs: List[Proof] = [] if check_spendable: - spendables = await self.check_spendable(proofs) - for i, spendable in enumerate(spendables.spendable): + proof_states = await self.check_proof_state(proofs) + for i, spendable in enumerate(proof_states.spendable): if not spendable: invalidated_proofs.append(proofs[i]) else: diff --git a/setup.py b/setup.py index 7025cc3..d4ebc5e 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]} setuptools.setup( name="cashu", - version="0.12.2", + version="0.12.3", description="Ecash wallet and mint for Bitcoin Lightning", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/conftest.py b/tests/conftest.py index 6f61c89..642147f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,20 +93,20 @@ async def ledger(): yield ledger -@pytest.fixture(autouse=True, scope="session") -def mint_3338(): - settings.mint_listen_port = 3338 - settings.port = 3338 - settings.mint_url = "http://localhost:3338" - settings.port = settings.mint_listen_port - config = uvicorn.Config( - "cashu.mint.app:app", - port=settings.mint_listen_port, - host="127.0.0.1", - ) +# @pytest.fixture(autouse=True, scope="session") +# def mint_3338(): +# settings.mint_listen_port = 3338 +# settings.port = 3338 +# settings.mint_url = "http://localhost:3338" +# settings.port = settings.mint_listen_port +# config = uvicorn.Config( +# "cashu.mint.app:app", +# port=settings.mint_listen_port, +# host="127.0.0.1", +# ) - server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY") - server.start() - time.sleep(1) - yield server - server.stop() +# server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY") +# server.start() +# time.sleep(1) +# yield server +# server.stop() diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index eae6bc1..87ea1ef 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -1,9 +1,11 @@ import asyncio +import json import pytest import pytest_asyncio import requests +from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof from cashu.core.settings import settings from tests.conftest import ledger @@ -54,3 +56,22 @@ async def test_api_mint_validation(ledger): assert "error" in response.json() response = requests.get(f"{BASE_URL}/mint?amount=1") assert "error" not in response.json() + + +@pytest.mark.asyncio +async def test_api_check_state(ledger): + proofs = [ + Proof(id="1234", amount=0, secret="asdasdasd", C="asdasdasd"), + Proof(id="1234", amount=0, secret="asdasdasd1", C="asdasdasd1"), + ] + payload = CheckSpendableRequest(proofs=proofs) + response = requests.post( + f"{BASE_URL}/check", + data=payload.json(), + ) + assert response.status_code == 200, f"{response.url} {response.status_code}" + states = CheckSpendableResponse.parse_obj(response.json()) + assert states.spendable + assert len(states.spendable) == 2 + assert states.pending + assert len(states.pending) == 2 diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 34c7a6b..a8e23c5 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -367,3 +367,12 @@ async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet): wallet1.proofs, 8, secret_lock ) # sender side await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver + + +@pytest.mark.asyncio +async def test_token_state(wallet1: Wallet): + await wallet1.mint(64) + assert wallet1.balance == 64 + resp = await wallet1.check_proof_state(wallet1.proofs) + assert resp.dict()["spendable"] + assert resp.dict()["pending"]