Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cashu/lightning/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 15 additions & 6 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from typing import List, Optional, Union

from fastapi import APIRouter
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably return HTTP 202 Accepted?

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

Expand Down Expand Up @@ -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 "
Expand Down
117 changes: 75 additions & 42 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import base64
import hashlib
import json
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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]
Expand All @@ -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