From 4490cc6fceec4ef2e1c3b1e89cfff15ab2c5e0e9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:12:10 +0200 Subject: [PATCH] Add get quote API to wallet + check proof states in batches (#637) * add get quote api to wallet * wrong string * test before pushing * fix tests for deprecated api only * sigh --- cashu/wallet/cli/cli.py | 19 +++++++++--------- cashu/wallet/v1_api.py | 38 ++++++++++++++++++++++++++++++++++- cashu/wallet/wallet.py | 27 ++++++++++++++++--------- tests/test_mint_operations.py | 26 +++++++++++++++++++++++- tests/test_wallet.py | 11 +++++++++- tests/test_wallet_cli.py | 3 +++ 6 files changed, 102 insertions(+), 22 deletions(-) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index abea8479..a1a723f1 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -381,8 +381,12 @@ def mint_invoice_callback(msg: JSONRPCNotficationParams): while time.time() < check_until and not paid: await asyncio.sleep(5) try: - await wallet.mint(amount, split=optional_split, id=invoice.id) - paid = True + mint_quote_resp = await wallet.get_mint_quote(invoice.id) + if mint_quote_resp.state == MintQuoteState.paid.value: + await wallet.mint(amount, split=optional_split, id=invoice.id) + paid = True + else: + print(".", end="", flush=True) except Exception as e: # TODO: user error codes! if "not paid" in str(e): @@ -710,12 +714,7 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str): if delete: await wallet.invalidate(proofs) else: - # invalidate proofs in batches - for _proofs in [ - proofs[i : i + settings.proofs_batch_size] - for i in range(0, len(proofs), settings.proofs_batch_size) - ]: - await wallet.invalidate(_proofs, check_spendable=True) + await wallet.invalidate(proofs, check_spendable=True) await print_balance(ctx) @@ -1024,7 +1023,9 @@ async def info(ctx: Context, mint: bool, mnemonic: bool): if mint_info.get("time"): print(f" - Server time: {mint_info['time']}") if mint_info.get("nuts"): - nuts_str = ', '.join([f"NUT-{k}" for k in mint_info['nuts'].keys()]) + nuts_str = ", ".join( + [f"NUT-{k}" for k in mint_info["nuts"].keys()] + ) print(f" - Supported NUTS: {nuts_str}") print("") except Exception as e: diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 3cb007c3..c4241226 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -170,7 +170,7 @@ async def _get_keys(self) -> List[WalletKeyset]: keys_dict: dict = resp.json() assert len(keys_dict), Exception("did not receive any keys") keys = KeysResponse.parse_obj(keys_dict) - keysets_str = ' '.join([f"{k.id} ({k.unit})" for k in keys.keysets]) + keysets_str = " ".join([f"{k.id} ({k.unit})" for k in keys.keysets]) logger.debug(f"Received {len(keys.keysets)} keysets from mint: {keysets_str}.") ret = [ WalletKeyset( @@ -312,6 +312,24 @@ async def mint_quote( return_dict = resp.json() return PostMintQuoteResponse.parse_obj(return_dict) + @async_set_httpx_client + @async_ensure_mint_loaded + async def get_mint_quote(self, quote: str) -> PostMintQuoteResponse: + """Returns an existing mint quote from the server. + + Args: + quote (str): Quote ID + + Returns: + PostMintQuoteResponse: Mint Quote Response + """ + resp = await self.httpx.get( + join(self.url, f"/v1/mint/quote/bolt11/{quote}"), + ) + self.raise_on_error_request(resp) + return_dict = resp.json() + return PostMintQuoteResponse.parse_obj(return_dict) + @async_set_httpx_client @async_ensure_mint_loaded async def mint( @@ -400,6 +418,24 @@ async def melt_quote( return_dict = resp.json() return PostMeltQuoteResponse.parse_obj(return_dict) + @async_set_httpx_client + @async_ensure_mint_loaded + async def get_melt_quote(self, quote: str) -> PostMeltQuoteResponse: + """Returns an existing melt quote from the server. + + Args: + quote (str): Quote ID + + Returns: + PostMeltQuoteResponse: Melt Quote Response + """ + resp = await self.httpx.get( + join(self.url, f"/v1/melt/quote/bolt11/{quote}"), + ) + self.raise_on_error_request(resp) + return_dict = resp.json() + return PostMeltQuoteResponse.parse_obj(return_dict) + @async_set_httpx_client @async_ensure_mint_loaded async def melt( diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index f621b7fd..d8915ed5 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -162,7 +162,7 @@ async def with_db( self.keysets = {k.id: k for k in keysets_active_unit} else: self.keysets = {k.id: k for k in keysets_list} - keysets_str = ' '.join([f"{i} {k.unit}" for i, k in self.keysets.items()]) + keysets_str = " ".join([f"{i} {k.unit}" for i, k in self.keysets.items()]) logger.debug(f"Loaded keysets: {keysets_str}") return self @@ -351,10 +351,9 @@ async def load_proofs(self, reload: bool = False, all_keysets=False) -> None: for keyset_id in self.keysets: proofs = await get_proofs(db=self.db, id=keyset_id, conn=conn) self.proofs.extend(proofs) - keysets_str = ' '.join([f"{k.id} ({k.unit})" for k in self.keysets.values()]) + keysets_str = " ".join([f"{k.id} ({k.unit})" for k in self.keysets.values()]) logger.trace(f"Proofs loaded for keysets: {keysets_str}") - async def load_keysets_from_db( self, url: Union[str, None] = "", unit: Union[str, None] = "" ): @@ -1020,10 +1019,15 @@ async def invalidate( """ invalidated_proofs: List[Proof] = [] if check_spendable: - proof_states = await self.check_proof_state(proofs) - for i, state in enumerate(proof_states.states): - if state.spent: - invalidated_proofs.append(proofs[i]) + # checks proofs in batches + for _proofs in [ + proofs[i : i + settings.proofs_batch_size] + for i in range(0, len(proofs), settings.proofs_batch_size) + ]: + proof_states = await self.check_proof_state(proofs) + for i, state in enumerate(proof_states.states): + if state.spent: + invalidated_proofs.append(proofs[i]) else: invalidated_proofs = proofs @@ -1033,9 +1037,12 @@ async def invalidate( f" {self.unit.str(sum_proofs(invalidated_proofs))}." ) - async with self.db.connect() as conn: - for p in invalidated_proofs: - await invalidate_proof(p, db=self.db, conn=conn) + for p in invalidated_proofs: + try: + # mark proof as spent + await invalidate_proof(p, db=self.db) + except Exception as e: + logger.error(f"DB error while invalidating proof: {e}") invalidate_secrets = [p.secret for p in invalidated_proofs] self.proofs = list( diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index a3fd3fd7..ab457759 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -4,6 +4,7 @@ from cashu.core.base import MeltQuoteState from cashu.core.helpers import sum_proofs from cashu.core.models import PostMeltQuoteRequest, PostMintQuoteRequest +from cashu.core.settings import settings from cashu.mint.ledger import Ledger from cashu.wallet.wallet import Wallet from cashu.wallet.wallet import Wallet as Wallet1 @@ -55,6 +56,13 @@ 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 ( + not melt_quote_response_pre_payment.state == MeltQuoteState.paid.value + ), "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 not melt_quote_pre_payment.paid, "melt quote should not be paid" assert melt_quote_pre_payment.unpaid @@ -89,6 +97,13 @@ 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.state == MeltQuoteState.unpaid.value + ), "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 not melt_quote_pre_payment.paid, "melt quote should not be paid" assert melt_quote_pre_payment.unpaid @@ -109,7 +124,12 @@ async def test_mint_internal(wallet1: Wallet, ledger: Ledger): mint_quote = await ledger.get_mint_quote(invoice.id) assert mint_quote.paid, "mint quote should be paid" - assert mint_quote.paid + + if not settings.debug_mint_only_deprecated: + mint_quote_resp = await wallet1.get_mint_quote(invoice.id) + assert ( + mint_quote_resp.state == MeltQuoteState.paid.value + ), "mint quote should be paid" output_amounts = [128] secrets, rs, derivation_paths = await wallet1.generate_n_secrets( @@ -139,6 +159,10 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger): assert not mint_quote.paid, "mint quote already paid" assert mint_quote.unpaid + if not settings.debug_mint_only_deprecated: + mint_quote_resp = await wallet1.get_mint_quote(quote.quote) + assert not mint_quote_resp.paid, "mint quote should not be paid" + await assert_err( wallet1.mint(128, id=quote.quote), "quote not paid", diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 076d3955..86963172 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -4,7 +4,7 @@ import pytest import pytest_asyncio -from cashu.core.base import Proof +from cashu.core.base import MintQuoteState, Proof from cashu.core.errors import CashuError, KeysetNotFoundError from cashu.core.helpers import sum_proofs from cashu.core.settings import settings @@ -168,6 +168,11 @@ async def test_request_mint(wallet1: Wallet): async def test_mint(wallet1: Wallet): invoice = await wallet1.request_mint(64) await pay_if_regtest(invoice.bolt11) + if not settings.debug_mint_only_deprecated: + quote_resp = await wallet1.get_mint_quote(invoice.id) + assert quote_resp.request == invoice.bolt11 + assert quote_resp.state == MintQuoteState.paid.value + expected_proof_amounts = wallet1.split_wallet_state(64) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 64 @@ -307,6 +312,10 @@ 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.amount == quote.amount + _, send_proofs = await wallet1.swap_to_send(wallet1.proofs, total_amount) melt_response = await wallet1.melt( diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 995518dd..bb4331a1 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -110,6 +110,9 @@ def test_balance(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,