diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index dfeed1004..e7364526f 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -76,6 +76,9 @@ async def create_invoice( return InvoiceResponse(True, checking_id, payment_request) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + # artificial sleep + await asyncio.sleep(5) + invoice = decode(bolt11) if invoice.payment_hash[:6] == self.privkey[:6] or BRR: diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 8f972d582..03dd44c4c 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -798,7 +798,7 @@ async def _generate_change_promises( return_amounts_sorted = sorted(return_amounts, reverse=True) # we need to imprint these amounts into the blanket outputs for i in range(len(outputs)): - outputs[i].amount = return_amounts_sorted[i] + outputs[i].amount = return_amounts_sorted[i] # type: ignore if not self._verify_no_duplicate_outputs(outputs): raise TransactionError("duplicate promises.") return_promises = await self._generate_promises(outputs, keyset) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 3ede2c6b3..ffea57245 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -1,3 +1,4 @@ +import asyncio from typing import List, Optional, Union from fastapi import APIRouter @@ -184,15 +185,23 @@ async def mint( " promises for change." ), ) -async def melt(payload: PostMeltRequest) -> GetMeltResponse: +async def melt(payload: PostMeltRequest, blocking: bool = True) -> GetMeltResponse: """ Requests tokens to be destroyed and sent out via Lightning. """ logger.trace(f"> POST /melt: {payload}") - ok, preimage, change_promises = await ledger.melt( - payload.proofs, payload.pr, payload.outputs - ) - resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises) + + # run asynchronously if blocking is False + if not blocking: + asyncio.create_task(ledger.melt(payload.proofs, payload.pr, payload.outputs)) + resp = GetMeltResponse(paid=False, preimage=None, change=None) + else: + # otherwise run synchronously + ok, preimage, change_promises = await ledger.melt( + payload.proofs, payload.pr, payload.outputs + ) + resp = GetMeltResponse(paid=ok, preimage=preimage, change=change_promises) + logger.trace(f"< POST /melt: {resp}") return resp @@ -291,7 +300,7 @@ async def split( @router.post( "/restore", name="Restore", - summary="Restores a blinded signature from a secret", + summary="Reissues a blinded signature for a blinded secret", response_model=PostRestoreResponse, response_description=( "Two lists with the first being the list of the provided outputs that " diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8926ffa78..5a93390d9 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1,3 +1,4 @@ +import asyncio import base64 import hashlib import json @@ -568,7 +569,11 @@ async def check_fees(self, payment_request: str): @async_set_requests async def pay_lightning( - self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]] + self, + proofs: List[Proof], + invoice: str, + outputs: Optional[List[BlindedMessage]], + blocking: bool = True, ): """ Accepts proofs and a lightning invoice to pay in exchange. @@ -588,10 +593,10 @@ def _meltrequest_include_fields(proofs: List[Proof]): resp = self.s.post( self.url + "/melt", json=payload.dict(include=_meltrequest_include_fields(proofs)), # type: ignore + params={"blocking": blocking}, ) self.raise_on_error(resp) return_dict = resp.json() - return GetMeltResponse.parse_obj(return_dict) @async_set_requests @@ -1151,37 +1156,39 @@ async def pay_lightning( secrets, rs, derivation_paths = await self.generate_n_secrets(n_return_outputs) outputs, rs = self._construct_outputs(n_return_outputs * [1], secrets, rs) - status = await super().pay_lightning(proofs, invoice, outputs) - - if status.paid: - # the payment was successful - await self.invalidate(proofs) - invoice_obj = Invoice( - amount=-sum_proofs(proofs), - pr=invoice, - preimage=status.preimage, - paid=True, - time_paid=time.time(), - hash="", - ) - # we have a unique constraint on the hash, so we generate a random one if it doesn't exist - invoice_obj.hash = invoice_obj.hash or await self._generate_secret() - await store_lightning_invoice(db=self.db, invoice=invoice_obj) - - # handle change and produce proofs - if status.change: - change_proofs = self._construct_proofs( - status.change, - secrets[: len(status.change)], - rs[: len(status.change)], - derivation_paths[: len(status.change)], - ) - logger.debug(f"Received change: {sum_proofs(change_proofs)} sat") - await self._store_proofs(change_proofs) + # pay_lightning returns immediatelly + _ = await super().pay_lightning(proofs, invoice, outputs, blocking=False) + + # we check the state of the proofs until they are spent + await asyncio.sleep(2) + spent = False + while not spent: + state = await self.check_proof_state(proofs) + assert state.pending, "pending response is empty." + spent = not any(state.pending) and not any(state.spendable) + await asyncio.sleep(1) + + # the payment was successful + await self.invalidate(proofs) + invoice_obj = Invoice( + amount=-sum_proofs(proofs), + pr=invoice, + # preimage=status.preimage, + paid=True, + time_paid=time.time(), + hash="", + ) + # we have a unique constraint on the hash, so we generate a random one if it doesn't exist + invoice_obj.hash = invoice_obj.hash or await self._generate_secret() + await store_lightning_invoice(db=self.db, invoice=invoice_obj) - else: - raise Exception("could not pay invoice.") - return status.paid + # request change for overpaid feeds + change_proofs = await self.restore_promises( + outputs=outputs, secrets=secrets, rs=rs, derivation_paths=derivation_paths + ) + logger.debug(f"Received change: {sum_proofs(change_proofs)} sat") + + return True async def check_proof_state(self, proofs): return await super().check_proof_state(proofs) @@ -1655,7 +1662,7 @@ async def restore_wallet_from_mnemonic( n_last_restored_proofs = 0 while stop_counter < to: print(f"Restoring token {i} to {i + batch}...") - restored_proofs = await self.restore_promises(i, i + batch - 1) + restored_proofs = await self.restore_promises_from_to(i, i + batch - 1) if len(restored_proofs) == 0: stop_counter += 1 spendable_proofs = await self.invalidate(restored_proofs) @@ -1679,7 +1686,9 @@ async def restore_wallet_from_mnemonic( print("No tokens restored.") return - async def restore_promises(self, from_counter: int, to_counter: int) -> List[Proof]: + async def restore_promises_from_to( + self, from_counter: int, to_counter: int + ) -> List[Proof]: """Restores promises from a given range of counters. This is for restoring a wallet from a mnemonic. Args: @@ -1698,14 +1707,42 @@ async def restore_promises(self, from_counter: int, to_counter: int) -> List[Pro # we generate outptus from deterministic secrets and rs regenerated_outputs, _ = self._construct_outputs(amounts_dummy, secrets, rs) # we ask the mint to reissue the promises - # restored_outputs is there so we can match the promises to the secrets and rs - restored_outputs, restored_promises = await super().restore_promises( - regenerated_outputs + proofs = await self.restore_promises( + outputs=regenerated_outputs, + secrets=secrets, + rs=rs, + derivation_paths=derivation_paths, ) + + await set_secret_derivation( + db=self.db, keyset_id=self.keyset_id, counter=to_counter + 1 + ) + return proofs + + async def restore_promises( + self, + outputs: List[BlindedMessage], + secrets: List[str], + rs: List[PrivateKey], + derivation_paths: List[str], + ) -> List[Proof]: + """Restores proofs from a list of outputs, secrets, rs and derivation paths. + + Args: + outputs (List[BlindedMessage]): Outputs for which we request promises + secrets (List[str]): Secrets generated for the outputs + rs (List[PrivateKey]): Random blinding factors generated for the outputs + derivation_paths (List[str]): Derivation paths for the secrets + + Returns: + List[Proof]: List of restored proofs + """ + # restored_outputs is there so we can match the promises to the secrets and rs + restored_outputs, restored_promises = await super().restore_promises(outputs) # now we need to filter out the secrets and rs that had a match matching_indices = [ idx - for idx, val in enumerate(regenerated_outputs) + for idx, val in enumerate(outputs) if val.B_ in [o.B_ for o in restored_outputs] ] secrets = [secrets[i] for i in matching_indices] @@ -1721,8 +1758,4 @@ async def restore_promises(self, from_counter: int, to_counter: int) -> List[Pro for proof in proofs: if proof.secret not in [p.secret for p in self.proofs]: self.proofs.append(proof) - - await set_secret_derivation( - db=self.db, keyset_id=self.keyset_id, counter=to_counter + 1 - ) return proofs