From 2c54c240ba316e8ffa4457e7cdc56faf072b2f75 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 11:08:46 +0300 Subject: [PATCH] feat: store generated invoices per mint --- lnbits/extensions/cashu/core/base.py | 167 ++++++++++++++++++++++++++ lnbits/extensions/cashu/crud.py | 52 +++++++- lnbits/extensions/cashu/migrations.py | 15 +++ lnbits/extensions/cashu/mint.py | 29 +++-- lnbits/extensions/cashu/views_api.py | 42 ++++--- 5 files changed, 276 insertions(+), 29 deletions(-) create mode 100644 lnbits/extensions/cashu/core/base.py diff --git a/lnbits/extensions/cashu/core/base.py b/lnbits/extensions/cashu/core/base.py new file mode 100644 index 00000000..0aa02442 --- /dev/null +++ b/lnbits/extensions/cashu/core/base.py @@ -0,0 +1,167 @@ +from sqlite3 import Row +from typing import List, Union + +from pydantic import BaseModel + + +class CashuError(BaseModel): + code = "000" + error = "CashuError" + + +class P2SHScript(BaseModel): + script: str + signature: str + address: Union[str, None] = None + + @classmethod + def from_row(cls, row: Row): + return cls( + address=row[0], + script=row[1], + signature=row[2], + used=row[3], + ) + + +class Proof(BaseModel): + amount: int + secret: str = "" + C: str + script: Union[P2SHScript, None] = None + reserved: bool = False # whether this proof is reserved for sending + send_id: str = "" # unique ID of send attempt + time_created: str = "" + time_reserved: str = "" + + @classmethod + def from_row(cls, row: Row): + return cls( + amount=row[0], + C=row[1], + secret=row[2], + reserved=row[3] or False, + send_id=row[4] or "", + time_created=row[5] or "", + time_reserved=row[6] or "", + ) + + @classmethod + def from_dict(cls, d: dict): + assert "amount" in d, "no amount in proof" + return cls( + amount=d.get("amount"), + C=d.get("C"), + secret=d.get("secret") or "", + reserved=d.get("reserved") or False, + send_id=d.get("send_id") or "", + time_created=d.get("time_created") or "", + time_reserved=d.get("time_reserved") or "", + ) + + def to_dict(self): + return dict(amount=self.amount, secret=self.secret, C=self.C) + + def to_dict_no_secret(self): + return dict(amount=self.amount, C=self.C) + + def __getitem__(self, key): + return self.__getattribute__(key) + + def __setitem__(self, key, val): + self.__setattr__(key, val) + + +class Proofs(BaseModel): + """TODO: Use this model""" + + proofs: List[Proof] + + +class Invoice(BaseModel): + amount: int + pr: str + hash: str + issued: bool = False + + @classmethod + def from_row(cls, row: Row): + return cls( + amount=int(row[0]), + pr=str(row[1]), + hash=str(row[2]), + issued=bool(row[3]), + ) + + +class BlindedMessage(BaseModel): + amount: int + B_: str + + +class BlindedSignature(BaseModel): + amount: int + C_: str + + @classmethod + def from_dict(cls, d: dict): + return cls( + amount=d["amount"], + C_=d["C_"], + ) + + +class MintRequest(BaseModel): + blinded_messages: List[BlindedMessage] = [] + + +class GetMintResponse(BaseModel): + pr: str + hash: str + + +class GetMeltResponse(BaseModel): + paid: Union[bool, None] + preimage: Union[str, None] + + +class SplitRequest(BaseModel): + proofs: List[Proof] + amount: int + output_data: Union[ + MintRequest, None + ] = None # backwards compatibility with clients < v0.2.2 + outputs: Union[MintRequest, None] = None + + def __init__(self, **data): + super().__init__(**data) + self.backwards_compatibility_v021() + + def backwards_compatibility_v021(self): + # before v0.2.2: output_data, after: outputs + if self.output_data: + self.outputs = self.output_data + self.output_data = None + + +class PostSplitResponse(BaseModel): + fst: List[BlindedSignature] + snd: List[BlindedSignature] + + +class CheckRequest(BaseModel): + proofs: List[Proof] + + +class CheckFeesRequest(BaseModel): + pr: str + + +class CheckFeesResponse(BaseModel): + fee: Union[int, None] + + +class MeltRequest(BaseModel): + proofs: List[Proof] + amount: int = None # deprecated + invoice: str diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py index 7a9c25c3..c991a8ec 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -1,6 +1,7 @@ import os from typing import List, Optional, Union +from .core.base import Invoice from lnbits.helpers import urlsafe_short_hash @@ -98,7 +99,7 @@ async def store_promise( ): promise_id = urlsafe_short_hash() - await (conn or db).execute( + await db.execute( """ INSERT INTO cashu.promises (id, amount, B_b, C_b, cashu_id) @@ -140,4 +141,51 @@ async def invalidate_proof( str(proof.secret), cashu_id ), - ) \ No newline at end of file + ) + + + + + + +######################################## +############ MINT INVOICES ############# +######################################## + + +async def store_lightning_invoice(cashu_id: str, invoice: Invoice): + await db.execute( + """ + INSERT INTO cashu.invoices + (cashu_id, amount, pr, hash, issued) + VALUES (?, ?, ?, ?, ?) + """, + ( + cashu_id, + invoice.amount, + invoice.pr, + invoice.hash, + invoice.issued, + ), + ) + +async def get_lightning_invoice(cashu_id: str, hash: str): + row = await db.fetchone( + """ + SELECT * from invoices + WHERE cashu_id =? AND hash = ? + """, + (cashu_id, hash,), + ) + return Invoice.from_row(row) + + +async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool): + await db.execute( + "UPDATE invoices SET issued = ? WHERE cashu_id = ? AND hash = ?", + ( + issued, + cashu_id, + hash, + ), + ) diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index f7d8f4f0..3f1df660 100644 --- a/lnbits/extensions/cashu/migrations.py +++ b/lnbits/extensions/cashu/migrations.py @@ -60,4 +60,19 @@ async def m001_initial(db): cashu_id TEXT NOT NULL ); """ + ) + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS cashu.invoices ( + cashu_id TEXT NOT NULL, + amount INTEGER NOT NULL, + pr TEXT NOT NULL, + hash TEXT NOT NULL, + issued BOOL NOT NULL, + + UNIQUE (hash) + + ); + """ ) \ No newline at end of file diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py index 70b6895e..fca096ed 100644 --- a/lnbits/extensions/cashu/mint.py +++ b/lnbits/extensions/cashu/mint.py @@ -11,13 +11,22 @@ def get_pubkeys(xpriv: str): return {a: p.serialize().hex() for a, p in pub_keys.items()} -async def request_mint(mint: Cashu, amount): - """Returns Lightning invoice and stores it in the db.""" - payment_request, checking_id = await self._request_lightning_invoice(amount) - invoice = Invoice( - amount=amount, pr=payment_request, hash=checking_id, issued=False - ) - if not payment_request or not checking_id: - raise Exception(f"Could not create Lightning invoice.") - await store_lightning_invoice(invoice, db=self.db) - return payment_request, checking_id \ No newline at end of file +# async def mint(self, B_s: List[PublicKey], amounts: List[int], payment_hash=None): +# """Mints a promise for coins for B_.""" +# # check if lightning invoice was paid +# if LIGHTNING: +# try: +# paid = await self._check_lightning_invoice(payment_hash) +# except: +# raise Exception("could not check invoice.") +# if not paid: +# raise Exception("Lightning invoice not paid yet.") + +# for amount in amounts: +# if amount not in [2**i for i in range(MAX_ORDER)]: +# raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.") + +# promises = [ +# await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) +# ] +# return promises \ No newline at end of file diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index a4f8a2d8..c0bc59f3 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -13,6 +13,7 @@ from lnbits.core.crud import get_user from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key +from .core.base import CashuError from . import cashu_ext from .ledger import request_mint, mint @@ -22,12 +23,13 @@ from .crud import ( create_cashu, delete_cashu, get_cashu, - get_cashus, - update_cashu_keys + get_cashus, + store_lightning_invoice, ) from .models import ( - Cashu, + Cashu, + Invoice, Pegs, CheckPayload, MeltPayload, @@ -215,7 +217,7 @@ async def keys(cashu_id: str = Query(False), wallet: WalletTypeInfo = Depends(ge @cashu_ext.get("/api/v1/mint/{cashu_id}") async def mint_pay_request(amount: int = 0, cashu_id: str = Query(None)): """Request minting of tokens. Server responds with a Lightning invoice.""" - print('############################') + print('############################ amount', amount) cashu = await get_cashu(cashu_id) if cashu is None: raise HTTPException( @@ -229,28 +231,34 @@ async def mint_pay_request(amount: int = 0, cashu_id: str = Query(None)): memo=f"{cashu.name}", extra={"tag": "cashu"}, ) + invoice = Invoice( + amount=amount, pr=payment_request, hash=payment_hash, issued=False + ) + await store_lightning_invoice(cashu_id, invoice) except Exception as e: - raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + logger.error(e) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(cashu_id)) - - print(f"Lightning invoice: {payment_request}") return {"pr": payment_request, "hash": payment_hash} @cashu_ext.post("/mint") async def mint_coins(payloads: MintPayloads, payment_hash: Union[str, None] = None, cashu_id: str = Query(None)): + """ + Requests the minting of tokens belonging to a paid payment request. + + Call this endpoint after `GET /mint`. + """ amounts = [] B_s = [] - for payload in payloads.blinded_messages: - amounts.append(payload.amount) - B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) - promises = await mint(B_s, amounts, payment_hash, cashu_id) - logger.debug(promises) - try: - promises = await mint(B_s, amounts, payment_hash, cashu_id) - return promises - except Exception as exc: - return {"error": str(exc)} + # for payload in payloads.blinded_messages: + # amounts.append(payload.amount) + # B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) + # try: + # promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash) + # return promises + # except Exception as exc: + # return CashuError(error=str(exc)) @cashu_ext.post("/melt")