From df0ddc1c45fe9a56da669af533871b33fd336467 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 10:09:16 +0300 Subject: [PATCH 01/53] feat: generate keys --- lnbits/extensions/cashu/core/secp.py | 52 ++++++++++++++++++++++++++ lnbits/extensions/cashu/mint.py | 12 ++++++ lnbits/extensions/cashu/mint_helper.py | 22 +++++++++++ lnbits/extensions/cashu/views_api.py | 23 ++++++++++-- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 lnbits/extensions/cashu/core/secp.py create mode 100644 lnbits/extensions/cashu/mint.py create mode 100644 lnbits/extensions/cashu/mint_helper.py diff --git a/lnbits/extensions/cashu/core/secp.py b/lnbits/extensions/cashu/core/secp.py new file mode 100644 index 000000000..334164344 --- /dev/null +++ b/lnbits/extensions/cashu/core/secp.py @@ -0,0 +1,52 @@ +from secp256k1 import PrivateKey, PublicKey + + +# We extend the public key to define some operations on points +# Picked from https://github.com/WTRMQDev/secp256k1-zkp-py/blob/master/secp256k1_zkp/__init__.py +class PublicKeyExt(PublicKey): + def __add__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + new_pub = PublicKey() + new_pub.combine([self.public_key, pubkey2.public_key]) + return new_pub + else: + raise TypeError("Cant add pubkey and %s" % pubkey2.__class__) + + def __neg__(self): + serialized = self.serialize() + first_byte, remainder = serialized[:1], serialized[1:] + # flip odd/even byte + first_byte = {b"\x03": b"\x02", b"\x02": b"\x03"}[first_byte] + return PublicKey(first_byte + remainder, raw=True) + + def __sub__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + return self + (-pubkey2) + else: + raise TypeError("Can't add pubkey and %s" % pubkey2.__class__) + + def mult(self, privkey): + if isinstance(privkey, PrivateKey): + return self.tweak_mul(privkey.private_key) + else: + raise TypeError("Can't multiply with non privatekey") + + def __eq__(self, pubkey2): + if isinstance(pubkey2, PublicKey): + seq1 = self.to_data() + seq2 = pubkey2.to_data() + return seq1 == seq2 + else: + raise TypeError("Can't compare pubkey and %s" % pubkey2.__class__) + + def to_data(self): + return [self.public_key.data[i] for i in range(64)] + + +# Horrible monkeypatching +PublicKey.__add__ = PublicKeyExt.__add__ +PublicKey.__neg__ = PublicKeyExt.__neg__ +PublicKey.__sub__ = PublicKeyExt.__sub__ +PublicKey.mult = PublicKeyExt.mult +PublicKey.__eq__ = PublicKeyExt.__eq__ +PublicKey.to_data = PublicKeyExt.to_data diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py new file mode 100644 index 000000000..703bf426d --- /dev/null +++ b/lnbits/extensions/cashu/mint.py @@ -0,0 +1,12 @@ + +from .crud import get_cashu +from .mint_helper import derive_keys, derive_pubkeys + + +def get_pubkeys(xpriv: str): + """Returns public keys for possible amounts.""" + + keys = derive_keys(xpriv) + pub_keys = derive_pubkeys(keys) + + return {a: p.serialize().hex() for a, p in pub_keys.items()} \ No newline at end of file diff --git a/lnbits/extensions/cashu/mint_helper.py b/lnbits/extensions/cashu/mint_helper.py new file mode 100644 index 000000000..30e66b033 --- /dev/null +++ b/lnbits/extensions/cashu/mint_helper.py @@ -0,0 +1,22 @@ +import hashlib +from typing import List, Set +from .core.secp import PrivateKey, PublicKey + +# todo: extract const +MAX_ORDER = 64 + +def derive_keys(master_key: str): + """Deterministic derivation of keys for 2^n values.""" + return { + 2 + ** i: PrivateKey( + hashlib.sha256((str(master_key) + str(i)).encode("utf-8")) + .hexdigest() + .encode("utf-8")[:32], + raw=True, + ) + for i in range(MAX_ORDER) + } + +def derive_pubkeys(keys: List[PrivateKey]): + return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} \ No newline at end of file diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 391aeda11..65323715a 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -15,7 +15,8 @@ from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from . import cashu_ext -from .ledger import get_pubkeys, request_mint, mint +from .ledger import request_mint, mint +from .mint import get_pubkeys from .crud import ( create_cashu, @@ -35,6 +36,11 @@ from .models import ( PayLnurlWData ) +######################################## +#################MINT CRUD############## +######################################## + +# todo: use /mints @cashu_ext.get("/api/v1/cashus", status_code=HTTPStatus.OK) async def api_cashus( all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) @@ -83,6 +89,9 @@ async def api_cashu_delete( raise HTTPException(status_code=HTTPStatus.NO_CONTENT) +######################################## +#################????################### +######################################## @cashu_ext.post("/api/v1/cashus/{cashu_id}/invoices", status_code=HTTPStatus.CREATED) async def api_cashu_create_invoice( amount: int = Query(..., ge=1), tipAmount: int = None, cashu_id: str = None @@ -192,10 +201,16 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str): #################MINT################### ######################################## -@cashu_ext.get("/keys") -def keys(cashu_id: str): +@cashu_ext.get("/api/v1/mint/keys/{cashu_id}", status_code=HTTPStatus.OK) +async def keys(cashu_id: str = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)): """Get the public keys of the mint""" - return get_pubkeys(cashu_id) + print('############################') + mint = await get_cashu(cashu_id) + if mint is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + return get_pubkeys(mint.prvkey) @cashu_ext.get("/mint") From 1ea8593de188e40a2156a630842b31f8ea3ef24b Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 10:23:42 +0300 Subject: [PATCH 02/53] feat: generate invoice for amount --- lnbits/extensions/cashu/mint.py | 15 +++++++++++++-- lnbits/extensions/cashu/views_api.py | 22 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py index 703bf426d..70b6895e4 100644 --- a/lnbits/extensions/cashu/mint.py +++ b/lnbits/extensions/cashu/mint.py @@ -1,5 +1,5 @@ -from .crud import get_cashu +from .models import Cashu from .mint_helper import derive_keys, derive_pubkeys @@ -9,4 +9,15 @@ def get_pubkeys(xpriv: str): keys = derive_keys(xpriv) pub_keys = derive_pubkeys(keys) - return {a: p.serialize().hex() for a, p in pub_keys.items()} \ No newline at end of file + 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 diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 65323715a..a4f8a2d8f 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -204,7 +204,6 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str): @cashu_ext.get("/api/v1/mint/keys/{cashu_id}", status_code=HTTPStatus.OK) async def keys(cashu_id: str = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)): """Get the public keys of the mint""" - print('############################') mint = await get_cashu(cashu_id) if mint is None: raise HTTPException( @@ -213,10 +212,27 @@ async def keys(cashu_id: str = Query(False), wallet: WalletTypeInfo = Depends(ge return get_pubkeys(mint.prvkey) -@cashu_ext.get("/mint") +@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.""" - payment_request, payment_hash = await request_mint(amount, cashu_id) + print('############################') + cashu = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + try: + payment_hash, payment_request = await create_invoice( + wallet_id=cashu.wallet, + amount=amount, + memo=f"{cashu.name}", + extra={"tag": "cashu"}, + ) + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + print(f"Lightning invoice: {payment_request}") return {"pr": payment_request, "hash": payment_hash} From 91a981a9ac525b46d83e9ee81db603b5b9b79ca5 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 11:08:46 +0300 Subject: [PATCH 03/53] 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 000000000..0aa024427 --- /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 7a9c25c3f..c991a8ecc 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 f7d8f4f0a..3f1df660a 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 70b6895e4..fca096ed7 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 a4f8a2d8f..c0bc59f39 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") From 33212e0b73f9e9c249121be83628f83a460c5516 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 11:17:02 +0300 Subject: [PATCH 04/53] chore: format --- lnbits/extensions/cashu/__init__.py | 3 + lnbits/extensions/cashu/crud.py | 84 +- lnbits/extensions/cashu/ledger.py | 100 +- lnbits/extensions/cashu/migrations.py | 2 +- lnbits/extensions/cashu/mint.py | 8 +- lnbits/extensions/cashu/mint_helper.py | 5 +- lnbits/extensions/cashu/models.py | 9 +- lnbits/extensions/cashu/tasks.py | 1 + .../cashu/templates/cashu/_api_docs.html | 3 +- .../cashu/templates/cashu/_cashu.html | 7 +- .../cashu/templates/cashu/index.html | 160 ++- .../cashu/templates/cashu/mint.html | 13 +- .../cashu/templates/cashu/wallet.html | 1104 ++++++++++------- lnbits/extensions/cashu/views.py | 9 +- lnbits/extensions/cashu/views_api.py | 94 +- 15 files changed, 992 insertions(+), 610 deletions(-) diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py index cf2776648..bd7d5513b 100644 --- a/lnbits/extensions/cashu/__init__.py +++ b/lnbits/extensions/cashu/__init__.py @@ -10,13 +10,16 @@ db = Database("ext_cashu") cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"]) + def cashu_renderer(): return template_renderer(["lnbits/extensions/cashu/templates"]) + from .tasks import wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa + def cashu_start(): loop = asyncio.get_event_loop() loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py index c991a8ecc..448614ac4 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -1,22 +1,18 @@ import os - +import random +from binascii import hexlify, unhexlify from typing import List, Optional, Union -from .core.base import Invoice + +from embit import bip32, bip39, ec, script +from embit.networks import NETWORKS +from loguru import logger from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Cashu, Pegs, Proof, Promises +from .core.base import Invoice +from .models import Cashu, Pegs, Promises, Proof -from embit import script -from embit import ec -from embit.networks import NETWORKS -from embit import bip32 -from embit import bip39 -from binascii import unhexlify, hexlify -import random - -from loguru import logger async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: cashu_id = urlsafe_short_hash() @@ -25,7 +21,7 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: mnemonic = bip39.mnemonic_from_bytes(entropy) seed = bip39.mnemonic_to_seed(mnemonic) root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) - + bip44_xprv = root.derive("m/44h/1h/0h") bip44_xpub = bip44_xprv.to_public() @@ -43,7 +39,7 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: data.maxsats, data.coins, bip44_xprv.to_base58(), - bip44_xpub.to_base58() + bip44_xpub.to_base58(), ), ) @@ -57,11 +53,16 @@ async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]: mnemonic = bip39.mnemonic_from_bytes(entropy) seed = bip39.mnemonic_to_seed(mnemonic) root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) - + bip44_xprv = root.derive("m/44h/1h/0h") bip44_xpub = bip44_xprv.to_public() - await db.execute("UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", bip44_xprv.to_base58(), bip44_xpub.to_base58(), cashu_id) + await db.execute( + "UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", + bip44_xprv.to_base58(), + bip44_xpub.to_base58(), + cashu_id, + ) row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) return Cashu(**row) if row else None @@ -91,12 +92,8 @@ async def delete_cashu(cashu_id) -> None: ###############MINT STUFF################# ########################################## -async def store_promise( - amount: int, - B_: str, - C_: str, - cashu_id -): + +async def store_promise(amount: int, B_: str, C_: str, cashu_id): promise_id = urlsafe_short_hash() await db.execute( @@ -105,28 +102,25 @@ async def store_promise( (id, amount, B_b, C_b, cashu_id) VALUES (?, ?, ?, ?, ?) """, - ( - promise_id, - amount, - str(B_), - str(C_), - cashu_id - ), + (promise_id, amount, str(B_), str(C_), cashu_id), ) + async def get_promises(cashu_id) -> Optional[Cashu]: - row = await db.fetchall("SELECT * FROM cashu.promises WHERE cashu_id = ?", (promises_id,)) + row = await db.fetchall( + "SELECT * FROM cashu.promises WHERE cashu_id = ?", (promises_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,)) + rows = await db.fetchall( + "SELECT secret from cashu.proofs_used WHERE id = ?", (cashu_id,) + ) return [row[0] for row in rows] -async def invalidate_proof( - proof: Proof, - cashu_id -): +async def invalidate_proof(proof: Proof, cashu_id): invalidate_proof_id = urlsafe_short_hash() await (conn or db).execute( """ @@ -134,20 +128,10 @@ async def invalidate_proof( (id, amount, C, secret, cashu_id) VALUES (?, ?, ?, ?, ?) """, - ( - invalidate_proof_id, - proof.amount, - str(proof.C), - str(proof.secret), - cashu_id - ), + (invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), cashu_id), ) - - - - ######################################## ############ MINT INVOICES ############# ######################################## @@ -169,18 +153,22 @@ async def store_lightning_invoice(cashu_id: str, invoice: Invoice): ), ) + 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,), + ( + cashu_id, + hash, + ), ) return Invoice.from_row(row) -async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool): +async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool): await db.execute( "UPDATE invoices SET issued = ? WHERE cashu_id = ? AND hash = ?", ( diff --git a/lnbits/extensions/cashu/ledger.py b/lnbits/extensions/cashu/ledger.py index 404f7ee8a..a28dc97aa 100644 --- a/lnbits/extensions/cashu/ledger.py +++ b/lnbits/extensions/cashu/ledger.py @@ -1,13 +1,15 @@ import hashlib from typing import List, Set -from .models import BlindedMessage, BlindedSignature, Invoice, Proof -from secp256k1 import PublicKey, PrivateKey - from fastapi import Query -from .crud import get_cashu +from secp256k1 import PrivateKey, PublicKey + from lnbits.core.services import check_transaction_status, create_invoice +from .crud import get_cashu +from .models import BlindedMessage, BlindedSignature, Invoice, Proof + + def _derive_keys(master_key: str, cashu_id: str = Query(None)): """Deterministic derivation of keys for 2^n values.""" return { @@ -21,29 +23,34 @@ def _derive_keys(master_key: str, cashu_id: str = Query(None)): for i in range(MAX_ORDER) } + def _derive_pubkeys(keys: List[PrivateKey], cashu_id: str = Query(None)): return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} -async def _generate_promises(amounts: List[int], B_s: List[str], cashu_id: str = Query(None)): + +async def _generate_promises( + amounts: List[int], B_s: List[str], cashu_id: str = Query(None) +): """Generates promises that sum to the given amount.""" return [ await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True)) for (amount, B_) in zip(amounts, B_s) ] + async def _generate_promise(amount: int, B_: PublicKey, cashu_id: str = Query(None)): """Generates a promise for given amount and returns a pair (amount, C').""" secret_key = self.keys[amount] # Get the correct key C_ = step2_bob(B_, secret_key) - await store_promise( - amount, B_=B_.serialize().hex(), C_=C_.serialize().hex() - ) + await store_promise(amount, B_=B_.serialize().hex(), C_=C_.serialize().hex()) return BlindedSignature(amount=amount, C_=C_.serialize().hex()) + def _check_spendable(proof: Proof, cashu_id: str = Query(None)): """Checks whether the proof was already spent.""" return not proof.secret in self.proofs_used + def _verify_proof(proof: Proof, cashu_id: str = Query(None)): """Verifies that the proof of promise was issued by this ledger.""" if not self._check_spendable(proof): @@ -52,7 +59,13 @@ def _verify_proof(proof: Proof, cashu_id: str = Query(None)): C = PublicKey(bytes.fromhex(proof.C), raw=True) return verify(secret_key, C, proof.secret) -def _verify_outputs(total: int, amount: int, output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_outputs( + total: int, + amount: int, + output_data: List[BlindedMessage], + cashu_id: str = Query(None), +): """Verifies the expected split was correctly computed""" fst_amt, snd_amt = total - amount, amount # we have two amounts to split to fst_outputs = amount_split(fst_amt) @@ -61,7 +74,10 @@ def _verify_outputs(total: int, amount: int, output_data: List[BlindedMessage], given = [o.amount for o in output_data] return given == expected -def _verify_no_duplicates(proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_no_duplicates( + proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None) +): secrets = [p.secret for p in proofs] if len(secrets) != len(list(set(secrets))): return False @@ -70,6 +86,7 @@ def _verify_no_duplicates(proofs: List[Proof], output_data: List[BlindedMessage] return False return True + def _verify_split_amount(amount: int, cashu_id: str = Query(None)): """Split amount like output amount can't be negative or too big.""" try: @@ -78,6 +95,7 @@ def _verify_split_amount(amount: int, cashu_id: str = Query(None)): # For better error message raise Exception("invalid split amount: " + str(amount)) + def _verify_amount(amount: int, cashu_id: str = Query(None)): """Any amount used should be a positive integer not larger than 2^MAX_ORDER.""" valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER @@ -85,12 +103,16 @@ def _verify_amount(amount: int, cashu_id: str = Query(None)): raise Exception("invalid amount: " + str(amount)) return amount -def _verify_equation_balanced(proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None)): + +def _verify_equation_balanced( + proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None) +): """Verify that Σoutputs - Σinputs = 0.""" sum_inputs = sum(self._verify_amount(p.amount) for p in proofs) sum_outputs = sum(self._verify_amount(p.amount) for p in outs) assert sum_outputs - sum_inputs == 0 + def _get_output_split(amount: int, cashu_id: str): """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" self._verify_amount(amount) @@ -101,6 +123,7 @@ def _get_output_split(amount: int, cashu_id: str): rv.append(2**pos) return rv + async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)): """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" # Mark proofs as used and prepare new promises @@ -110,10 +133,12 @@ async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)): for p in proofs: await invalidate_proof(p) + def get_pubkeys(cashu_id: str = Query(None)): """Returns public keys for possible amounts.""" return {a: p.serialize().hex() for a, p in self.pub_keys.items()} + async def request_mint(amount, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: @@ -125,9 +150,7 @@ async def request_mint(amount, cashu_id: str = Query(None)): amount=amount, memo=cashu.name, unhashed_description=cashu.name.encode("utf-8"), - extra={ - "tag": "Cashu" - }, + extra={"tag": "Cashu"}, ) invoice = Invoice( @@ -137,15 +160,23 @@ async def request_mint(amount, cashu_id: str = Query(None)): raise Exception(f"Could not create Lightning invoice.") return payment_request, payment_hash -async def mint(B_s: List[PublicKey], amounts: List[int], payment_hash: str = Query(None), cashu_id: str = Query(None)): + +async def mint( + B_s: List[PublicKey], + amounts: List[int], + payment_hash: str = Query(None), + cashu_id: str = Query(None), +): cashu = await get_cashu(cashu_id) if not cashu: raise Exception(f"Could not find Cashu") """Mints a promise for coins for B_.""" # check if lightning invoice was paid - if payment_hash: - if not await check_transaction_status(wallet_id=cashu.wallet, payment_hash=payment_hash): + if payment_hash: + if not await check_transaction_status( + wallet_id=cashu.wallet, payment_hash=payment_hash + ): raise Exception("Lightning invoice not paid yet.") for amount in amounts: @@ -157,11 +188,14 @@ async def mint(B_s: List[PublicKey], amounts: List[int], payment_hash: str = Que ] return promises -async def melt(proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None)): + +async def melt( + proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None) +): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Invalidates proofs and pays a Lightning invoice.""" # if not LIGHTNING: total = sum([p["amount"] for p in proofs]) @@ -181,6 +215,7 @@ async def melt(proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Q await self._invalidate_proofs(proofs) return status, payment_hash + async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: @@ -189,7 +224,13 @@ async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): """Checks if all provided proofs are valid and still spendable (i.e. have not been spent).""" return {i: self._check_spendable(p) for i, p in enumerate(proofs)} -async def split(proofs: List[Proof], amount: int, output_data: List[BlindedMessage], cashu_id: str = Query(None)): + +async def split( + proofs: List[Proof], + amount: int, + output_data: List[BlindedMessage], + cashu_id: str = Query(None), +): cashu = await get_cashu(cashu_id) if not cashu: raise Exception(f"Could not find Cashu") @@ -226,18 +267,19 @@ async def split(proofs: List[Proof], amount: int, output_data: List[BlindedMessa async def fee_reserve(amount_msat: int, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Function for calculating the Lightning fee reserve""" return max( int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) ) + async def amount_split(amount, cashu_id: str): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" bits_amt = bin(amount)[::-1][:-2] rv = [] @@ -246,11 +288,12 @@ async def amount_split(amount, cashu_id: str): rv.append(2**pos) return rv + async def hash_to_point(secret_msg, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") - + raise Exception(f"Could not find Cashu") + """Generates x coordinate from the message hash and checks if the point lies on the curve. If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" point = None @@ -280,10 +323,11 @@ async def step1_alice(secret_msg, cashu_id: str = Query(None)): B_ = Y + r.pubkey return B_, r + async def step2_bob(B_, a, cashu_id: str = Query(None)): cashu = await get_cashu(cashu_id) if not cashu: - raise Exception(f"Could not find Cashu") + raise Exception(f"Could not find Cashu") C_ = B_.mult(a) return C_ diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index 3f1df660a..cb6b24f96 100644 --- a/lnbits/extensions/cashu/migrations.py +++ b/lnbits/extensions/cashu/migrations.py @@ -75,4 +75,4 @@ async def m001_initial(db): ); """ - ) \ No newline at end of file + ) diff --git a/lnbits/extensions/cashu/mint.py b/lnbits/extensions/cashu/mint.py index fca096ed7..c25683137 100644 --- a/lnbits/extensions/cashu/mint.py +++ b/lnbits/extensions/cashu/mint.py @@ -1,16 +1,16 @@ - -from .models import Cashu from .mint_helper import derive_keys, derive_pubkeys +from .models import Cashu def get_pubkeys(xpriv: str): """Returns public keys for possible amounts.""" - + keys = derive_keys(xpriv) pub_keys = derive_pubkeys(keys) return {a: p.serialize().hex() for a, p in pub_keys.items()} + # 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 @@ -29,4 +29,4 @@ def get_pubkeys(xpriv: str): # promises = [ # await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) # ] -# return promises \ No newline at end of file +# return promises diff --git a/lnbits/extensions/cashu/mint_helper.py b/lnbits/extensions/cashu/mint_helper.py index 30e66b033..1cf631b4a 100644 --- a/lnbits/extensions/cashu/mint_helper.py +++ b/lnbits/extensions/cashu/mint_helper.py @@ -1,10 +1,12 @@ import hashlib from typing import List, Set + from .core.secp import PrivateKey, PublicKey # todo: extract const MAX_ORDER = 64 + def derive_keys(master_key: str): """Deterministic derivation of keys for 2^n values.""" return { @@ -18,5 +20,6 @@ def derive_keys(master_key: str): for i in range(MAX_ORDER) } + def derive_pubkeys(keys: List[PrivateKey]): - return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} \ No newline at end of file + return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py index 094966ff5..570387a28 100644 --- a/lnbits/extensions/cashu/models.py +++ b/lnbits/extensions/cashu/models.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Optional, List +from typing import List, Optional from fastapi import Query from pydantic import BaseModel @@ -20,20 +20,22 @@ class Cashu(BaseModel): def from_row(cls, row: Row) -> "TPoS": return cls(**dict(row)) + class Pegs(BaseModel): id: str wallet: str inout: str amount: str - @classmethod def from_row(cls, row: Row) -> "TPoS": return cls(**dict(row)) + class PayLnurlWData(BaseModel): lnurl: str + class Promises(BaseModel): id: str amount: int @@ -41,6 +43,7 @@ class Promises(BaseModel): C_b: str cashu_id: str + class Proof(BaseModel): amount: int secret: str @@ -142,4 +145,4 @@ class CheckPayload(BaseModel): class MeltPayload(BaseModel): proofs: List[Proof] amount: int - invoice: str \ No newline at end of file + invoice: str diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py index 5fbdde8ed..fe00a5918 100644 --- a/lnbits/extensions/cashu/tasks.py +++ b/lnbits/extensions/cashu/tasks.py @@ -9,6 +9,7 @@ from lnbits.tasks import internal_invoice_queue, register_invoice_listener from .crud import get_cashu + async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue) diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html index 7378eb084..3476d41aa 100644 --- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html +++ b/lnbits/extensions/cashu/templates/cashu/_api_docs.html @@ -71,7 +71,8 @@
Curl example
curl -X DELETE {{ request.base_url - }}cashu/api/v1/cashus/<cashu_id> -H "X-Api-Key: <admin_key>" + }}cashu/api/v1/cashus/<cashu_id> -H "X-Api-Key: + <admin_key>" diff --git a/lnbits/extensions/cashu/templates/cashu/_cashu.html b/lnbits/extensions/cashu/templates/cashu/_cashu.html index 3c2a38f53..f5af738f1 100644 --- a/lnbits/extensions/cashu/templates/cashu/_cashu.html +++ b/lnbits/extensions/cashu/templates/cashu/_cashu.html @@ -2,13 +2,12 @@

- Make Ecash mints with peg in/out to a wallet, that can create and manage ecash. + Make Ecash mints with peg in/out to a wallet, that can create and manage + ecash.

Created by - Calle.Calle.
diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html index 3cd57d45d..37dc360ef 100644 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ b/lnbits/extensions/cashu/templates/cashu/index.html @@ -4,7 +4,9 @@
- New Mint + New Mint @@ -18,8 +20,14 @@ Export to CSV
- + {% raw %}