From d519605c5012ce3426704702ec594655c72af77d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 10:40:59 +0200 Subject: [PATCH 1/6] add pending state --- cashu/core/base.py | 1 + cashu/mint/ledger.py | 30 +++++++++++++++++++++++------- cashu/mint/router.py | 11 ++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 4945b812e..3004f216c 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -253,6 +253,7 @@ class CheckSpendableRequest(BaseModel): class CheckSpendableResponse(BaseModel): spendable: List[bool] + pending: List[bool] class CheckFeesRequest(BaseModel): diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index fe5d90365..cea7c506e 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -2,7 +2,7 @@ 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,17 @@ def _check_spendable(self, proof: Proof): """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_states: List[bool] = [] + for p in proofs: + if p.secret in [pp.secret for pp in proofs_pending]: + pending_states.append(True) + else: + pending_states.append(False) + 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 +826,14 @@ async def melt( 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 +841,11 @@ async def check_spendable(self, proofs: List[Proof]): 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 728000d85..d8bde402a 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( From efafaaf3f04244abd5f6da3438dabacf228c31e9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 11:04:47 +0200 Subject: [PATCH 2/6] proofs spendable check and tests --- cashu/wallet/wallet.py | 18 +++++++++--------- tests/test_mint_api.py | 20 +++++++++++++++++++- tests/test_wallet.py | 7 +++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 93065dc99..35a1caf1e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -498,13 +498,13 @@ def _splitrequest_include_fields(proofs): 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 @@ def _check_spendable_include_fields(proofs): 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 @@ async def pay_lightning(self, proofs: List[Proof], invoice: str, fee_reserve: in 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 @@ async def invalidate(self, proofs: List[Proof], check_spendable=True): """ 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/tests/test_mint_api.py b/tests/test_mint_api.py index eae6bc147..94de2cb4a 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -3,7 +3,8 @@ import pytest import pytest_asyncio import requests - +import json +from cashu.core.base import Proof, CheckSpendableRequest, CheckSpendableResponse from cashu.core.settings import settings from tests.conftest import ledger @@ -54,3 +55,20 @@ 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 len(states.spendable) == 2 + assert len(states.pending) == 2 diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 34c7a6bc8..d3290f2f7 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -367,3 +367,10 @@ 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) From 2a9a4f06e44137d89f49ee8f01d115357f9755c6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 22:33:59 +0200 Subject: [PATCH 3/6] bump version to 0.12.3 --- README.md | 2 +- cashu/core/base.py | 4 +++- cashu/core/settings.py | 2 +- cashu/mint/ledger.py | 1 + cashu/wallet/cli/cli.py | 1 + setup.py | 2 +- tests/conftest.py | 34 +++++++++++++++++----------------- tests/test_mint_api.py | 7 +++++-- tests/test_wallet.py | 2 ++ 9 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d197cd12a..e2ed34141 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 3004f216c..d0c749435 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -253,7 +253,9 @@ class CheckSpendableRequest(BaseModel): class CheckSpendableResponse(BaseModel): spendable: List[bool] - pending: List[bool] + pending: Optional[ + List[bool] + ] = None # TODO: Uncomment when all mints are updated to report 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 80583b3ad..3dd79fcd7 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -8,7 +8,7 @@ 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 cea7c506e..272d1ca98 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -476,6 +476,7 @@ async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): preimage, error_message, ) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fee_limit_msat) + await asyncio.sleep(10) logger.trace(f"_pay_lightning_invoice: Lightning payment status: {ok}") # make sure that fee is positive fee_msat = abs(fee_msat) if fee_msat else fee_msat diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 1314a85d0..d0f7bf470 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/setup.py b/setup.py index 7025cc385..d4ebc5e13 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ 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 6f61c89cb..642147f36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,20 +93,20 @@ async def start_mint_init(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", - ) - - server = UvicornServer(config=config, private_key="SECOND_PRIVATE_KEY") - server.start() - time.sleep(1) - yield server - server.stop() +# @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() diff --git a/tests/test_mint_api.py b/tests/test_mint_api.py index 94de2cb4a..87ea1efbf 100644 --- a/tests/test_mint_api.py +++ b/tests/test_mint_api.py @@ -1,10 +1,11 @@ import asyncio +import json import pytest import pytest_asyncio import requests -import json -from cashu.core.base import Proof, CheckSpendableRequest, CheckSpendableResponse + +from cashu.core.base import CheckSpendableRequest, CheckSpendableResponse, Proof from cashu.core.settings import settings from tests.conftest import ledger @@ -70,5 +71,7 @@ async def test_api_check_state(ledger): ) 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 d3290f2f7..a8e23c5fa 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -374,3 +374,5 @@ 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"] From 18d024f0863101cf7493e6e59afccef2dcbd92f3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 22:44:40 +0200 Subject: [PATCH 4/6] remove sleep for testing --- cashu/mint/ledger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 272d1ca98..cea7c506e 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -476,7 +476,6 @@ async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): preimage, error_message, ) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fee_limit_msat) - await asyncio.sleep(10) logger.trace(f"_pay_lightning_invoice: Lightning payment status: {ok}") # make sure that fee is positive fee_msat = abs(fee_msat) if fee_msat else fee_msat From 5cc9a303fd76ed5ffffe50c8ea67a76c97a6c424 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 22:46:28 +0200 Subject: [PATCH 5/6] comment clarify --- cashu/core/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d0c749435..c068056be 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -255,7 +255,8 @@ class CheckSpendableResponse(BaseModel): spendable: List[bool] pending: Optional[ List[bool] - ] = None # TODO: Uncomment when all mints are updated to report pending tokens (kept for backwards compatibility of new wallets with old mints) + ] = 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): From ea7084910c2b3844e84ec3f442117ead1e80cc12 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 8 Jul 2023 22:48:30 +0200 Subject: [PATCH 6/6] use list comprehension in pending list --- cashu/mint/ledger.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index cea7c506e..f5c6ffdcf 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -189,12 +189,10 @@ def _check_spendable(self, proof: Proof): 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_states: List[bool] = [] - for p in proofs: - if p.secret in [pp.secret for pp in proofs_pending]: - pending_states.append(True) - else: - pending_states.append(False) + 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]: