diff --git a/lnbits/extensions/cashu/crud.py b/lnbits/extensions/cashu/crud.py index 2892e6a47..7a9c25c3f 100644 --- a/lnbits/extensions/cashu/crud.py +++ b/lnbits/extensions/cashu/crud.py @@ -1,24 +1,37 @@ +import os + from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import Cashu, Pegs, Proof +from .models import Cashu, Pegs, Proof, Promises 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() - prv = ec.PrivateKey.from_wif(urlsafe_short_hash()) - pub = prv.get_public_key() + + entropy = bytes([random.getrandbits(8) for i in range(16)]) + 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( """ INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, prvkey, pubkey) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( cashu_id, @@ -28,8 +41,8 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: data.fraction, data.maxsats, data.coins, - prv, - pub + bip44_xprv.to_base58(), + bip44_xpub.to_base58() ), ) @@ -39,17 +52,20 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]: - if not wif: - prv = ec.PrivateKey.from_wif(urlsafe_short_hash()) - else: - prv = ec.PrivateKey.from_wif(wif) - pub = prv.get_public_key() - await db.execute("UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", (hexlify(prv.serialize()), hexlify(pub.serialize()), cashu_id)) + entropy = bytes([random.getrandbits(8) for i in range(16)]) + 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) row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) return Cashu(**row) if row else None -async def get_cashu(cashu_id: str) -> Optional[Cashu]: +async def get_cashu(cashu_id) -> Optional[Cashu]: row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) return Cashu(**row) if row else None @@ -66,57 +82,62 @@ async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]: return [Cashu(**row) for row in rows] -async def delete_cashu(cashu_id: str) -> None: +async def delete_cashu(cashu_id) -> None: await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,)) - +########################################## ###############MINT STUFF################# +########################################## async def store_promise( amount: int, B_: str, - C_: str + C_: str, + cashu_id ): + promise_id = urlsafe_short_hash() await (conn or db).execute( """ - INSERT INTO promises - (amount, B_b, C_b) - VALUES (?, ?, ?) + INSERT INTO cashu.promises + (id, amount, B_b, C_b, cashu_id) + VALUES (?, ?, ?, ?, ?) """, ( + 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,)) + return Promises(**row) if row else None -async def get_proofs_used(): - - rows = await (conn or db).fetchall( - """ - SELECT secret from proofs_used - """ - ) +async def get_proofs_used(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 + proof: Proof, + cashu_id ): - - # we add the proof and secret to the used list + invalidate_proof_id = urlsafe_short_hash() await (conn or db).execute( """ - INSERT INTO proofs_used - (amount, C, secret) - VALUES (?, ?, ?) + INSERT INTO cashu.proofs_used + (id, amount, C, secret, cashu_id) + VALUES (?, ?, ?, ?, ?) """, ( + invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), + cashu_id ), ) \ No newline at end of file diff --git a/lnbits/extensions/cashu/ledger.py b/lnbits/extensions/cashu/ledger.py index 59cc3b922..404f7ee8a 100644 --- a/lnbits/extensions/cashu/ledger.py +++ b/lnbits/extensions/cashu/ledger.py @@ -5,234 +5,239 @@ from .models import BlindedMessage, BlindedSignature, Invoice, Proof from secp256k1 import PublicKey, PrivateKey from fastapi import Query - +from .crud import get_cashu from lnbits.core.services import check_transaction_status, create_invoice -class Ledger: - def __init__(self, secret_key: str, MAX_ORDER: int = Query(64)): - self.proofs_used: Set[str] = set() - - self.master_key: str = secret_key - self.keys: List[PrivateKey] = self._derive_keys(self.master_key) - self.pub_keys: List[PublicKey] = self._derive_pubkeys(self.keys) - - async def load_used_proofs(self): - self.proofs_used = set(await get_proofs_used) - - @staticmethod - 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) - } - - @staticmethod - def _derive_pubkeys(keys: List[PrivateKey]): - return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} - - async def _generate_promises(self, amounts: List[int], B_s: List[str]): - """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(self, amount: int, B_: PublicKey): - """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() +def _derive_keys(master_key: str, cashu_id: str = Query(None)): + """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, ) - return BlindedSignature(amount=amount, C_=C_.serialize().hex()) + for i in range(MAX_ORDER) + } - def _check_spendable(self, proof: Proof): - """Checks whether the proof was already spent.""" - return not proof.secret in self.proofs_used +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)]} - def _verify_proof(self, proof: Proof): - """Verifies that the proof of promise was issued by this ledger.""" - if not self._check_spendable(proof): - raise Exception(f"tokens already spent. Secret: {proof.secret}") - secret_key = self.keys[proof.amount] # Get the correct key to check against - C = PublicKey(bytes.fromhex(proof.C), raw=True) - return verify(secret_key, C, proof.secret) +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) + ] - def _verify_outputs( - self, total: int, amount: int, output_data: List[BlindedMessage] - ): - """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) - snd_outputs = amount_split(snd_amt) - expected = fst_outputs + snd_outputs - given = [o.amount for o in output_data] - return given == expected +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() + ) + return BlindedSignature(amount=amount, C_=C_.serialize().hex()) - def _verify_no_duplicates( - self, proofs: List[Proof], output_data: List[BlindedMessage] - ): - secrets = [p.secret for p in proofs] - if len(secrets) != len(list(set(secrets))): - return False - B_s = [od.B_ for od in output_data] - if len(B_s) != len(list(set(B_s))): - return False - return True +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_split_amount(self, amount: int): - """Split amount like output amount can't be negative or too big.""" - try: - self._verify_amount(amount) - except: - # For better error message - raise Exception("invalid split amount: " + str(amount)) +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): + raise Exception(f"tokens already spent. Secret: {proof.secret}") + secret_key = self.keys[proof.amount] # Get the correct key to check against + C = PublicKey(bytes.fromhex(proof.C), raw=True) + return verify(secret_key, C, proof.secret) - def _verify_amount(self, amount: int): - """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 - if not valid: - raise Exception("invalid amount: " + str(amount)) - return amount +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) + snd_outputs = amount_split(snd_amt) + expected = fst_outputs + snd_outputs + given = [o.amount for o in output_data] + return given == expected - def _verify_equation_balanced( - self, proofs: List[Proof], outs: List[BlindedMessage] - ): - """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 _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 + B_s = [od.B_ for od in output_data] + if len(B_s) != len(list(set(B_s))): + return False + return True - def _get_output_split(self, amount: int): - """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" +def _verify_split_amount(amount: int, cashu_id: str = Query(None)): + """Split amount like output amount can't be negative or too big.""" + try: self._verify_amount(amount) - bits_amt = bin(amount)[::-1][:-2] - rv = [] - for (pos, bit) in enumerate(bits_amt): - if bit == "1": - rv.append(2**pos) - return rv + except: + # For better error message + raise Exception("invalid split amount: " + str(amount)) - async def _invalidate_proofs(self, proofs: List[Proof]): - """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" - # Mark proofs as used and prepare new promises - proof_msgs = set([p.secret for p in proofs]) - self.proofs_used |= proof_msgs - # store in db - for p in proofs: - await invalidate_proof(p) +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 + if not valid: + raise Exception("invalid amount: " + str(amount)) + return amount - # Public methods - def get_pubkeys(self): - """Returns public keys for possible amounts.""" - return {a: p.serialize().hex() for a, p in self.pub_keys.items()} +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 - async def request_mint(self, amount): - """Returns Lightning invoice and stores it in the db.""" - payment_request, payment_hash = payment_hash, payment_request = await create_invoice( - wallet_id=link.wallet, - amount=amount, - memo=link.description, - unhashed_description=link.description.encode("utf-8"), - extra={ - "tag": "Cashu" - }, - ) +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) + bits_amt = bin(amount)[::-1][:-2] + rv = [] + for (pos, bit) in enumerate(bits_amt): + if bit == "1": + rv.append(2**pos) + return rv - invoice = Invoice( - amount=amount, pr=payment_request, hash=payment_hash, issued=False - ) - if not payment_request or not payment_hash: - raise Exception(f"Could not create Lightning invoice.") - await store_lightning_invoice(invoice) - return payment_request, payment_hash +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 + proof_msgs = set([p.secret for p in proofs]) + self.proofs_used |= proof_msgs + # store in db + for p in proofs: + await invalidate_proof(p) - 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 payment_hash and not await check_transaction_status(payment_hash): +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: + raise Exception(f"Could not find Cashu") + + """Returns Lightning invoice and stores it in the db.""" + payment_hash, payment_request = await create_invoice( + wallet_id=cashu.wallet, + amount=amount, + memo=cashu.name, + unhashed_description=cashu.name.encode("utf-8"), + extra={ + "tag": "Cashu" + }, + ) + + invoice = Invoice( + amount=amount, pr=payment_request, hash=payment_hash, issued=False + ) + if not payment_request or not payment_hash: + 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)): + 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): 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}.") + 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 + promises = [ + await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) + ] + return promises - async def melt(self, proofs: List[Proof], amount: int, invoice: str): - """Invalidates proofs and pays a Lightning invoice.""" - # if not LIGHTNING: - total = sum([p["amount"] for p in proofs]) - # check that lightning fees are included - assert total + fee_reserve(amount * 1000) >= amount, Exception( - "provided proofs not enough for Lightning payment." - ) +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") + + """Invalidates proofs and pays a Lightning invoice.""" + # if not LIGHTNING: + total = sum([p["amount"] for p in proofs]) + # check that lightning fees are included + assert total + fee_reserve(amount * 1000) >= amount, Exception( + "provided proofs not enough for Lightning payment." + ) - status, payment_hash = await pay_invoice( - wallet_id=link.wallet, - payment_request=invoice, - max_sat=amount, - extra={"tag": "Ecash melt"}, - ) + status, payment_hash = await pay_invoice( + wallet_id=link.wallet, + payment_request=invoice, + max_sat=amount, + extra={"tag": "Ecash melt"}, + ) - if status == True: - await self._invalidate_proofs(proofs) - return status, payment_hash - - async def check_spendable(self, proofs: List[Proof]): - """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( - self, proofs: List[Proof], amount: int, output_data: List[BlindedMessage] - ): - """Consumes proofs and prepares new promises based on the amount split.""" - self._verify_split_amount(amount) - # Verify proofs are valid - if not all([self._verify_proof(p) for p in proofs]): - return False - - total = sum([p.amount for p in proofs]) - - if not self._verify_no_duplicates(proofs, output_data): - raise Exception("duplicate proofs or promises") - if amount > total: - raise Exception("split amount is higher than the total sum") - if not self._verify_outputs(total, amount, output_data): - raise Exception("split of promises is not as expected") - - # Mark proofs as used and prepare new promises + if status == True: await self._invalidate_proofs(proofs) + return status, payment_hash - outs_fst = amount_split(total - amount) - outs_snd = amount_split(amount) - B_fst = [od.B_ for od in output_data[: len(outs_fst)]] - B_snd = [od.B_ for od in output_data[len(outs_fst) :]] - prom_fst, prom_snd = await self._generate_promises( - outs_fst, B_fst - ), await self._generate_promises(outs_snd, B_snd) - self._verify_equation_balanced(proofs, prom_fst + prom_snd) - return prom_fst, prom_snd +async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): + cashu = await get_cashu(cashu_id) + if not cashu: + raise Exception(f"Could not find Cashu") + + """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)): + cashu = await get_cashu(cashu_id) + if not cashu: + raise Exception(f"Could not find Cashu") + + """Consumes proofs and prepares new promises based on the amount split.""" + self._verify_split_amount(amount) + # Verify proofs are valid + if not all([self._verify_proof(p) for p in proofs]): + return False + + total = sum([p.amount for p in proofs]) + + if not self._verify_no_duplicates(proofs, output_data): + raise Exception("duplicate proofs or promises") + if amount > total: + raise Exception("split amount is higher than the total sum") + if not self._verify_outputs(total, amount, output_data): + raise Exception("split of promises is not as expected") + + # Mark proofs as used and prepare new promises + await self._invalidate_proofs(proofs) + + outs_fst = amount_split(total - amount) + outs_snd = amount_split(amount) + B_fst = [od.B_ for od in output_data[: len(outs_fst)]] + B_snd = [od.B_ for od in output_data[len(outs_fst) :]] + prom_fst, prom_snd = await self._generate_promises( + outs_fst, B_fst + ), await self._generate_promises(outs_snd, B_snd) + self._verify_equation_balanced(proofs, prom_fst + prom_snd) + return prom_fst, prom_snd -##############FUNCTIONS############### -def fee_reserve(amount_msat: int) -> int: +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") + """Function for calculating the Lightning fee reserve""" return max( int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) ) -def amount_split(amount): +async def amount_split(amount, cashu_id: str): + cashu = await get_cashu(cashu_id) + if not 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 = [] @@ -241,7 +246,11 @@ def amount_split(amount): rv.append(2**pos) return rv -def hash_to_point(secret_msg): +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") + """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 @@ -260,24 +269,39 @@ def hash_to_point(secret_msg): return point -def step1_alice(secret_msg): +async def step1_alice(secret_msg, cashu_id: str = Query(None)): + cashu = await get_cashu(cashu_id) + if not cashu: + raise Exception(f"Could not find Cashu") + secret_msg = secret_msg.encode("utf-8") Y = hash_to_point(secret_msg) r = PrivateKey() 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") -def step2_bob(B_, a): C_ = B_.mult(a) return C_ -def step3_alice(C_, r, A): +async def step3_alice(C_, r, A, cashu_id: str = Query(None)): + cashu = await get_cashu(cashu_id) + if not cashu: + raise Exception(f"Could not find Cashu") + C = C_ - A.mult(r) return C -def verify(a, C, secret_msg): +async def verify(a, C, secret_msg, cashu_id: str = Query(None)): + cashu = await get_cashu(cashu_id) + if not cashu: + raise Exception(f"Could not find Cashu") + Y = hash_to_point(secret_msg.encode("utf-8")) return C == Y.mult(a) diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index 534200624..f7d8f4f0a 100644 --- a/lnbits/extensions/cashu/migrations.py +++ b/lnbits/extensions/cashu/migrations.py @@ -8,7 +8,7 @@ async def m001_initial(db): id TEXT PRIMARY KEY, wallet TEXT NOT NULL, name TEXT NOT NULL, - tickershort TEXT NOT NULL, + tickershort TEXT DEFAULT 'sats', fraction BOOL, maxsats INT, coins INT, @@ -32,3 +32,32 @@ async def m001_initial(db): """ ) + """ + Initial cashus table. + """ + await db.execute( + """ + CREATE TABLE cashu.promises ( + id TEXT PRIMARY KEY, + amount INT, + B_b TEXT NOT NULL, + C_b TEXT NOT NULL, + cashu_id TEXT NOT NULL + ); + """ + ) + + """ + Initial cashus table. + """ + await db.execute( + """ + CREATE TABLE cashu.proofs_used ( + id TEXT PRIMARY KEY, + amount INT, + C TEXT NOT NULL, + secret TEXT NOT NULL, + cashu_id TEXT NOT NULL + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/cashu/models.py b/lnbits/extensions/cashu/models.py index a673dfe78..094966ff5 100644 --- a/lnbits/extensions/cashu/models.py +++ b/lnbits/extensions/cashu/models.py @@ -9,7 +9,7 @@ class Cashu(BaseModel): id: str = Query(None) name: str = Query(None) wallet: str = Query(None) - tickershort: str + tickershort: str = Query(None) fraction: bool = Query(None) maxsats: int = Query(0) coins: int = Query(0) @@ -34,6 +34,13 @@ class Pegs(BaseModel): class PayLnurlWData(BaseModel): lnurl: str +class Promises(BaseModel): + id: str + amount: int + B_b: str + C_b: str + cashu_id: str + class Proof(BaseModel): amount: int secret: str diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py index fe00a5918..5fbdde8ed 100644 --- a/lnbits/extensions/cashu/tasks.py +++ b/lnbits/extensions/cashu/tasks.py @@ -9,7 +9,6 @@ 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/index.html b/lnbits/extensions/cashu/templates/cashu/index.html index 17b2a9194..3cd57d45d 100644 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ b/lnbits/extensions/cashu/templates/cashu/index.html @@ -80,13 +80,10 @@ -
-
@@ -96,6 +93,8 @@
+
Create Mint + :disable="formDialog.data.wallet == null || formDialog.data.name == null" type="submit">Create Mint Cancel
diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py index 4ac1f1cea..655ed0289 100644 --- a/lnbits/extensions/cashu/views.py +++ b/lnbits/extensions/cashu/views.py @@ -22,7 +22,6 @@ async def index(request: Request, user: User = Depends(check_user_exists)): "cashu/index.html", {"request": request, "user": user.dict()} ) - @cashu_ext.get("/wallet") async def cashu(request: Request): return cashu_renderer().TemplateResponse("cashu/wallet.html",{"request": request}) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 5cc6e2713..391aeda11 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -15,10 +15,25 @@ from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from . import cashu_ext -from .crud import create_cashu, delete_cashu, get_cashu, get_cashus, update_cashu_keys -from .models import Cashu, Pegs, CheckPayload, MeltPayload, MintPayloads, SplitPayload, PayLnurlWData +from .ledger import get_pubkeys, request_mint, mint -from .ledger import Ledger, fee_reserve, amount_split, hash_to_point, step1_alice, step2_bob, step3_alice, verify +from .crud import ( + create_cashu, + delete_cashu, + get_cashu, + get_cashus, + update_cashu_keys +) + +from .models import ( + Cashu, + Pegs, + CheckPayload, + MeltPayload, + MintPayloads, + SplitPayload, + PayLnurlWData +) @cashu_ext.get("/api/v1/cashus", status_code=HTTPStatus.OK) async def api_cashus( @@ -173,50 +188,54 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str): return status -#################CASHU STUFF################### +######################################## +#################MINT################### +######################################## @cashu_ext.get("/keys") -def keys(): +def keys(cashu_id: str): """Get the public keys of the mint""" - return ledger.get_pubkeys() + return get_pubkeys(cashu_id) @cashu_ext.get("/mint") -async def request_mint(amount: int = 0): +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 ledger.request_mint(amount) + payment_request, payment_hash = await request_mint(amount, cashu_id) print(f"Lightning invoice: {payment_request}") return {"pr": payment_request, "hash": payment_hash} @cashu_ext.post("/mint") -async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None): +async def mint_coins(payloads: MintPayloads, payment_hash: Union[str, None] = None, cashu_id: str = Query(None)): 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 ledger.mint(B_s, amounts, payment_hash=payment_hash) + promises = await mint(B_s, amounts, payment_hash, cashu_id) return promises except Exception as exc: return {"error": str(exc)} @cashu_ext.post("/melt") -async def melt(payload: MeltPayload): +async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)): - ok, preimage = await ledger.melt(payload.proofs, payload.amount, payload.invoice) + ok, preimage = await melt(payload.proofs, payload.amount, payload.invoice, cashu_id) return {"paid": ok, "preimage": preimage} @cashu_ext.post("/check") -async def check_spendable(payload: CheckPayload): - return await ledger.check_spendable(payload.proofs) +async def check_spendable_coins(payload: CheckPayload, cashu_id: str = Query(None)): + return await check_spendable(payload.proofs, cashu_id) @cashu_ext.post("/split") -async def split(payload: SplitPayload): +async def spli_coinst(payload: SplitPayload, cashu_id: str = Query(None)): """ Requetst a set of tokens with amount "total" to be split into two newly minted sets with amount "split" and "total-split". @@ -225,7 +244,7 @@ async def split(payload: SplitPayload): amount = payload.amount output_data = payload.output_data.blinded_messages try: - split_return = await ledger.split(proofs, amount, output_data) + split_return = await split(proofs, amount, output_data) except Exception as exc: return {"error": str(exc)} if not split_return: