From 104aca33ff875bf676661f89255454cb3e4d898c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 11:17:02 +0300 Subject: [PATCH] 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 %}