diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py index 698a8089..c8f3c72b 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -93,7 +93,9 @@ async def delete_cashu(cashu_id) -> None: ########################################## -async def store_promises(amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str): +async def store_promises( + amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str +): for amount, B_, C_ in zip(amounts, B_s, C_s): await store_promise(amount, B_, C_, cashu_id) @@ -113,21 +115,21 @@ async def store_promise(amount: int, B_: str, C_: str, cashu_id: str): async def get_promises(cashu_id) -> Optional[Cashu]: row = await db.fetchall( - "SELECT * FROM cashu.promises WHERE cashu_id = ?", (promises_id,) + "SELECT * FROM cashu.promises WHERE cashu_id = ?", (cashu_id,) ) return Promises(**row) if row else None async def get_proofs_used(cashu_id): rows = await db.fetchall( - "SELECT secret from cashu.proofs_used WHERE id = ?", (cashu_id,) + "SELECT secret from cashu.proofs_used WHERE cashu_id = ?", (cashu_id,) ) return [row[0] for row in rows] -async def invalidate_proof(proof: Proof, cashu_id): +async def invalidate_proof(cashu_id: str, proof: Proof): invalidate_proof_id = urlsafe_short_hash() - await (conn or db).execute( + await db.execute( """ INSERT INTO cashu.proofs_used (id, amount, C, secret, cashu_id) diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py index 1be07b91..6be60703 100644 --- a/lnbits/extensions/cashu/mint.py +++ b/lnbits/extensions/cashu/mint.py @@ -1,9 +1,16 @@ -from typing import List +import math +from typing import List, Set + +from lnbits import bolt11 +from lnbits.core.services import check_transaction_status, fee_reserve, pay_invoice +from lnbits.extensions.cashu.models import Cashu +from lnbits.wallets.base import PaymentStatus from .core.b_dhke import step2_bob -from .core.base import BlindedSignature +from .core.base import BlindedSignature, Proof from .core.secp import PublicKey -from .mint_helper import derive_keys, derive_pubkeys +from .crud import get_proofs_used, invalidate_proof +from .mint_helper import derive_keys, derive_pubkeys, verify_proof # todo: extract const MAX_ORDER = 64 @@ -39,3 +46,52 @@ async def generate_promise(master_prvkey: str, amount: int, B_: PublicKey): secret_key = derive_keys(master_prvkey)[amount] # Get the correct key C_ = step2_bob(B_, secret_key) return BlindedSignature(amount=amount, C_=C_.serialize().hex()) + + +async def melt(cashu: Cashu, proofs: List[Proof], invoice: str): + """Invalidates proofs and pays a Lightning invoice.""" + # Verify proofs + proofs_used: Set[str] = set(await get_proofs_used(cashu.id)) + # if not all([verify_proof(cashu.prvkey, proofs_used, p) for p in proofs]): + # raise Exception("could not verify proofs.") + for p in proofs: + await verify_proof(cashu.prvkey, proofs_used, p) + + total_provided = sum([p["amount"] for p in proofs]) + invoice_obj = bolt11.decode(invoice) + amount = math.ceil(invoice_obj.amount_msat / 1000) + + fees_msat = await check_fees(cashu.wallet, invoice_obj) + assert total_provided >= amount + fees_msat / 1000, Exception( + "provided proofs not enough for Lightning payment." + ) + + await pay_invoice( + wallet_id=cashu.wallet, + payment_request=invoice, + description=f"pay cashu invoice", + extra={"tag": "cashu", "cahsu_name": cashu.name}, + ) + + status: PaymentStatus = await check_transaction_status( + cashu.wallet, invoice_obj.payment_hash + ) + if status.paid == True: + await invalidate_proofs(cashu.id, proofs) + return status.paid, status.preimage + return False, "" + + +async def check_fees(wallet_id: str, decoded_invoice): + """Returns the fees (in msat) required to pay this pr.""" + amount = math.ceil(decoded_invoice.amount_msat / 1000) + status: PaymentStatus = await check_transaction_status( + wallet_id, decoded_invoice.payment_hash + ) + fees_msat = fee_reserve(amount * 1000) if status.paid != True else 0 + return fees_msat + +async def invalidate_proofs(cashu_id: str, proofs: List[Proof]): + """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" + for p in proofs: + await invalidate_proof(cashu_id, p) \ No newline at end of file diff --git a/lnbits/extensions/cashu/mint_helper.py b/lnbits/extensions/cashu/mint_helper.py index cfb3b7d7..5fc43e49 100644 --- a/lnbits/extensions/cashu/mint_helper.py +++ b/lnbits/extensions/cashu/mint_helper.py @@ -1,7 +1,9 @@ import hashlib -from typing import List +from typing import List, Set -from .core.secp import PrivateKey +from .core.b_dhke import verify +from .core.secp import PrivateKey, PublicKey +from .models import Proof # todo: extract const MAX_ORDER = 64 @@ -21,6 +23,19 @@ def derive_keys(master_key: str): } - def derive_pubkeys(keys: List[PrivateKey]): return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} + + +async def verify_proof(master_prvkey: str, proofs_used: Set[str], proof: Proof): + """Verifies that the proof of promise was issued by this ledger.""" + if proof.secret in proofs_used: + raise Exception(f"tokens already spent. Secret: {proof.secret}") + + secret_key = derive_keys(master_prvkey)[ + proof.amount + ] # Get the correct key to check against + C = PublicKey(bytes.fromhex(proof.C), raw=True) + validMintSig = verify(secret_key, C, proof.secret) + if validMintSig != True: + raise Exception(f"tokens not valid. Secret: {proof.secret}") diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py index 8b7558d2..596db047 100644 --- a/lnbits/extensions/cashu/models.py +++ b/lnbits/extensions/cashu/models.py @@ -146,9 +146,3 @@ class MeltPayload(BaseModel): proofs: List[Proof] amount: int invoice: str - - -class CreateTokens(BaseModel): - # cashu_id: str = Query(None) - payloads: MintPayloads - payment_hash: Union[str, None] = None diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index d6b34538..bd1d27f0 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -28,11 +28,10 @@ from .crud import ( update_lightning_invoice, ) from .ledger import mint, request_mint -from .mint import generate_promises, get_pubkeys +from .mint import generate_promises, get_pubkeys, melt from .models import ( Cashu, CheckPayload, - CreateTokens, Invoice, MeltPayload, MintPayloads, @@ -248,7 +247,7 @@ async def mint_pay_request( except Exception as e: logger.error(e) raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(cashu_id) + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) ) return {"pr": payment_request, "hash": payment_hash} @@ -265,7 +264,6 @@ async def mint_coins( Requests the minting of tokens belonging to a paid payment request. Call this endpoint after `GET /mint`. """ - print("############################ amount") cashu: Cashu = await get_cashu(cashu_id) if cashu is None: raise HTTPException( @@ -282,12 +280,11 @@ async def mint_coins( ) if invoice.issued == True: raise HTTPException( - status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Tokens already issued for this invoice." + status_code=HTTPStatus.PAYMENT_REQUIRED, + detail="Tokens already issued for this invoice.", ) - status: PaymentStatus = await check_transaction_status( - cashu.wallet, payment_hash - ) + status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) # todo: revert to: status.paid != True: if status.paid == False: raise HTTPException( @@ -302,21 +299,33 @@ async def mint_coins( amounts.append(payload.amount) B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) - promises = await generate_promises(cashu.prvkey, amounts, B_s) for amount, B_, p in zip(amounts, B_s, promises): await store_promise(amount, B_.serialize().hex(), p.C_, cashu_id) - + return promises - except Exception as exc: - return CashuError(error=str(exc)) + except Exception as e: + logger.error(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) + ) -@cashu_ext.post("/melt") +@cashu_ext.post("/api/v1/melt/{cashu_id}") async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)): - - ok, preimage = await melt(payload.proofs, payload.amount, payload.invoice, cashu_id) - return {"paid": ok, "preimage": preimage} + cashu: Cashu = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + try: + ok, preimage = await melt(cashu, payload.proofs, payload.invoice) + return {"paid": ok, "preimage": preimage} + except Exception as e: + logger.error(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e) + ) @cashu_ext.post("/check")