Working through, getting functions working

This commit is contained in:
ben
2022-10-03 14:43:02 +01:00
parent 77269ad0a7
commit d51103e7e8
8 changed files with 354 additions and 257 deletions

View File

@@ -1,24 +1,37 @@
import os
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import Cashu, Pegs, Proof from .models import Cashu, Pegs, Proof, Promises
from embit import script from embit import script
from embit import ec from embit import ec
from embit.networks import NETWORKS from embit.networks import NETWORKS
from embit import bip32
from embit import bip39
from binascii import unhexlify, hexlify from binascii import unhexlify, hexlify
import random
from loguru import logger
async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: async def create_cashu(wallet_id: str, data: Cashu) -> Cashu:
cashu_id = urlsafe_short_hash() 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( await db.execute(
""" """
INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, prvkey, pubkey) INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, prvkey, pubkey)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
cashu_id, cashu_id,
@@ -28,8 +41,8 @@ async def create_cashu(wallet_id: str, data: Cashu) -> Cashu:
data.fraction, data.fraction,
data.maxsats, data.maxsats,
data.coins, data.coins,
prv, bip44_xprv.to_base58(),
pub 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]: async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]:
if not wif: entropy = bytes([random.getrandbits(8) for i in range(16)])
prv = ec.PrivateKey.from_wif(urlsafe_short_hash()) mnemonic = bip39.mnemonic_from_bytes(entropy)
else: seed = bip39.mnemonic_to_seed(mnemonic)
prv = ec.PrivateKey.from_wif(wif) root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
pub = prv.get_public_key()
await db.execute("UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", (hexlify(prv.serialize()), hexlify(pub.serialize()), cashu_id)) 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,)) row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
return Cashu(**row) if row else None 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,)) row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
return Cashu(**row) if row else None 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] 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,)) await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
##########################################
###############MINT STUFF################# ###############MINT STUFF#################
##########################################
async def store_promise( async def store_promise(
amount: int, amount: int,
B_: str, B_: str,
C_: str C_: str,
cashu_id
): ):
promise_id = urlsafe_short_hash()
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO promises INSERT INTO cashu.promises
(amount, B_b, C_b) (id, amount, B_b, C_b, cashu_id)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
( (
promise_id,
amount, amount,
str(B_), str(B_),
str(C_), 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(): async def get_proofs_used(cashu_id):
rows = await db.fetchall("SELECT secret from cashu.proofs_used WHERE id = ?", (cashu_id,))
rows = await (conn or db).fetchall(
"""
SELECT secret from proofs_used
"""
)
return [row[0] for row in rows] return [row[0] for row in rows]
async def invalidate_proof( async def invalidate_proof(
proof: Proof proof: Proof,
cashu_id
): ):
invalidate_proof_id = urlsafe_short_hash()
# we add the proof and secret to the used list
await (conn or db).execute( await (conn or db).execute(
""" """
INSERT INTO proofs_used INSERT INTO cashu.proofs_used
(amount, C, secret) (id, amount, C, secret, cashu_id)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
( (
invalidate_proof_id,
proof.amount, proof.amount,
str(proof.C), str(proof.C),
str(proof.secret), str(proof.secret),
cashu_id
), ),
) )

View File

@@ -5,234 +5,239 @@ from .models import BlindedMessage, BlindedSignature, Invoice, Proof
from secp256k1 import PublicKey, PrivateKey from secp256k1 import PublicKey, PrivateKey
from fastapi import Query from fastapi import Query
from .crud import get_cashu
from lnbits.core.services import check_transaction_status, create_invoice from lnbits.core.services import check_transaction_status, create_invoice
class Ledger: def _derive_keys(master_key: str, cashu_id: str = Query(None)):
def __init__(self, secret_key: str, MAX_ORDER: int = Query(64)): """Deterministic derivation of keys for 2^n values."""
self.proofs_used: Set[str] = set() return {
2
self.master_key: str = secret_key ** i: PrivateKey(
self.keys: List[PrivateKey] = self._derive_keys(self.master_key) hashlib.sha256((str(master_key) + str(i)).encode("utf-8"))
self.pub_keys: List[PublicKey] = self._derive_pubkeys(self.keys) .hexdigest()
.encode("utf-8")[:32],
async def load_used_proofs(self): raw=True,
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()
) )
return BlindedSignature(amount=amount, C_=C_.serialize().hex()) for i in range(MAX_ORDER)
}
def _check_spendable(self, proof: Proof): def _derive_pubkeys(keys: List[PrivateKey], cashu_id: str = Query(None)):
"""Checks whether the proof was already spent.""" return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
return not proof.secret in self.proofs_used
def _verify_proof(self, proof: Proof): async def _generate_promises(amounts: List[int], B_s: List[str], cashu_id: str = Query(None)):
"""Verifies that the proof of promise was issued by this ledger.""" """Generates promises that sum to the given amount."""
if not self._check_spendable(proof): return [
raise Exception(f"tokens already spent. Secret: {proof.secret}") await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True))
secret_key = self.keys[proof.amount] # Get the correct key to check against for (amount, B_) in zip(amounts, B_s)
C = PublicKey(bytes.fromhex(proof.C), raw=True) ]
return verify(secret_key, C, proof.secret)
def _verify_outputs( async def _generate_promise(amount: int, B_: PublicKey, cashu_id: str = Query(None)):
self, total: int, amount: int, output_data: List[BlindedMessage] """Generates a promise for given amount and returns a pair (amount, C')."""
): secret_key = self.keys[amount] # Get the correct key
"""Verifies the expected split was correctly computed""" C_ = step2_bob(B_, secret_key)
fst_amt, snd_amt = total - amount, amount # we have two amounts to split to await store_promise(
fst_outputs = amount_split(fst_amt) amount, B_=B_.serialize().hex(), C_=C_.serialize().hex()
snd_outputs = amount_split(snd_amt) )
expected = fst_outputs + snd_outputs return BlindedSignature(amount=amount, C_=C_.serialize().hex())
given = [o.amount for o in output_data]
return given == expected
def _verify_no_duplicates( def _check_spendable(proof: Proof, cashu_id: str = Query(None)):
self, proofs: List[Proof], output_data: List[BlindedMessage] """Checks whether the proof was already spent."""
): return not proof.secret in self.proofs_used
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 _verify_split_amount(self, amount: int): def _verify_proof(proof: Proof, cashu_id: str = Query(None)):
"""Split amount like output amount can't be negative or too big.""" """Verifies that the proof of promise was issued by this ledger."""
try: if not self._check_spendable(proof):
self._verify_amount(amount) raise Exception(f"tokens already spent. Secret: {proof.secret}")
except: secret_key = self.keys[proof.amount] # Get the correct key to check against
# For better error message C = PublicKey(bytes.fromhex(proof.C), raw=True)
raise Exception("invalid split amount: " + str(amount)) return verify(secret_key, C, proof.secret)
def _verify_amount(self, amount: int): def _verify_outputs(total: int, amount: int, output_data: List[BlindedMessage], cashu_id: str = Query(None)):
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER.""" """Verifies the expected split was correctly computed"""
valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER fst_amt, snd_amt = total - amount, amount # we have two amounts to split to
if not valid: fst_outputs = amount_split(fst_amt)
raise Exception("invalid amount: " + str(amount)) snd_outputs = amount_split(snd_amt)
return amount expected = fst_outputs + snd_outputs
given = [o.amount for o in output_data]
return given == expected
def _verify_equation_balanced( def _verify_no_duplicates(proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None)):
self, proofs: List[Proof], outs: List[BlindedMessage] secrets = [p.secret for p in proofs]
): if len(secrets) != len(list(set(secrets))):
"""Verify that Σoutputs - Σinputs = 0.""" return False
sum_inputs = sum(self._verify_amount(p.amount) for p in proofs) B_s = [od.B_ for od in output_data]
sum_outputs = sum(self._verify_amount(p.amount) for p in outs) if len(B_s) != len(list(set(B_s))):
assert sum_outputs - sum_inputs == 0 return False
return True
def _get_output_split(self, amount: int): def _verify_split_amount(amount: int, cashu_id: str = Query(None)):
"""Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" """Split amount like output amount can't be negative or too big."""
try:
self._verify_amount(amount) self._verify_amount(amount)
bits_amt = bin(amount)[::-1][:-2] except:
rv = [] # For better error message
for (pos, bit) in enumerate(bits_amt): raise Exception("invalid split amount: " + str(amount))
if bit == "1":
rv.append(2**pos)
return rv
async def _invalidate_proofs(self, proofs: List[Proof]): def _verify_amount(amount: int, cashu_id: str = Query(None)):
"""Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" """Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
# Mark proofs as used and prepare new promises valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER
proof_msgs = set([p.secret for p in proofs]) if not valid:
self.proofs_used |= proof_msgs raise Exception("invalid amount: " + str(amount))
# store in db return amount
for p in proofs:
await invalidate_proof(p)
# Public methods def _verify_equation_balanced(proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None)):
def get_pubkeys(self): """Verify that Σoutputs - Σinputs = 0."""
"""Returns public keys for possible amounts.""" sum_inputs = sum(self._verify_amount(p.amount) for p in proofs)
return {a: p.serialize().hex() for a, p in self.pub_keys.items()} sum_outputs = sum(self._verify_amount(p.amount) for p in outs)
assert sum_outputs - sum_inputs == 0
async def request_mint(self, amount): def _get_output_split(amount: int, cashu_id: str):
"""Returns Lightning invoice and stores it in the db.""" """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
payment_request, payment_hash = payment_hash, payment_request = await create_invoice( self._verify_amount(amount)
wallet_id=link.wallet, bits_amt = bin(amount)[::-1][:-2]
amount=amount, rv = []
memo=link.description, for (pos, bit) in enumerate(bits_amt):
unhashed_description=link.description.encode("utf-8"), if bit == "1":
extra={ rv.append(2**pos)
"tag": "Cashu" return rv
},
)
invoice = Invoice( async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)):
amount=amount, pr=payment_request, hash=payment_hash, issued=False """Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
) # Mark proofs as used and prepare new promises
if not payment_request or not payment_hash: proof_msgs = set([p.secret for p in proofs])
raise Exception(f"Could not create Lightning invoice.") self.proofs_used |= proof_msgs
await store_lightning_invoice(invoice) # store in db
return payment_request, payment_hash for p in proofs:
await invalidate_proof(p)
async def mint(self, B_s: List[PublicKey], amounts: List[int], payment_hash=None): def get_pubkeys(cashu_id: str = Query(None)):
"""Mints a promise for coins for B_.""" """Returns public keys for possible amounts."""
# check if lightning invoice was paid return {a: p.serialize().hex() for a, p in self.pub_keys.items()}
if payment_hash and not await check_transaction_status(payment_hash):
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.") raise Exception("Lightning invoice not paid yet.")
for amount in amounts: for amount in amounts:
if amount not in [2**i for i in range(MAX_ORDER)]: if amount not in [2**i for i in range(MAX_ORDER)]:
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.") raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
promises = [ promises = [
await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts)
] ]
return promises return promises
async def melt(self, proofs: List[Proof], amount: int, invoice: str): async def melt(proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None)):
"""Invalidates proofs and pays a Lightning invoice.""" cashu = await get_cashu(cashu_id)
# if not LIGHTNING: if not cashu:
total = sum([p["amount"] for p in proofs]) raise Exception(f"Could not find Cashu")
# check that lightning fees are included
assert total + fee_reserve(amount * 1000) >= amount, Exception( """Invalidates proofs and pays a Lightning invoice."""
"provided proofs not enough for Lightning payment." # 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( status, payment_hash = await pay_invoice(
wallet_id=link.wallet, wallet_id=link.wallet,
payment_request=invoice, payment_request=invoice,
max_sat=amount, max_sat=amount,
extra={"tag": "Ecash melt"}, extra={"tag": "Ecash melt"},
) )
if status == True: 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
await self._invalidate_proofs(proofs) await self._invalidate_proofs(proofs)
return status, payment_hash
outs_fst = amount_split(total - amount) async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)):
outs_snd = amount_split(amount) cashu = await get_cashu(cashu_id)
B_fst = [od.B_ for od in output_data[: len(outs_fst)]] if not cashu:
B_snd = [od.B_ for od in output_data[len(outs_fst) :]] raise Exception(f"Could not find Cashu")
prom_fst, prom_snd = await self._generate_promises(
outs_fst, B_fst """Checks if all provided proofs are valid and still spendable (i.e. have not been spent)."""
), await self._generate_promises(outs_snd, B_snd) return {i: self._check_spendable(p) for i, p in enumerate(proofs)}
self._verify_equation_balanced(proofs, prom_fst + prom_snd)
return prom_fst, prom_snd 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############### async def fee_reserve(amount_msat: int, cashu_id: str = Query(None)):
def fee_reserve(amount_msat: int) -> int: cashu = await get_cashu(cashu_id)
if not cashu:
raise Exception(f"Could not find Cashu")
"""Function for calculating the Lightning fee reserve""" """Function for calculating the Lightning fee reserve"""
return max( return max(
int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) 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].""" """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8]."""
bits_amt = bin(amount)[::-1][:-2] bits_amt = bin(amount)[::-1][:-2]
rv = [] rv = []
@@ -241,7 +246,11 @@ def amount_split(amount):
rv.append(2**pos) rv.append(2**pos)
return rv 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. """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.""" If it does not, it tries computing again a new x coordinate from the hash of the coordinate."""
point = None point = None
@@ -260,24 +269,39 @@ def hash_to_point(secret_msg):
return point 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") secret_msg = secret_msg.encode("utf-8")
Y = hash_to_point(secret_msg) Y = hash_to_point(secret_msg)
r = PrivateKey() r = PrivateKey()
B_ = Y + r.pubkey B_ = Y + r.pubkey
return B_, r 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) C_ = B_.mult(a)
return C_ 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) C = C_ - A.mult(r)
return C 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")) Y = hash_to_point(secret_msg.encode("utf-8"))
return C == Y.mult(a) return C == Y.mult(a)

View File

@@ -8,7 +8,7 @@ async def m001_initial(db):
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
tickershort TEXT NOT NULL, tickershort TEXT DEFAULT 'sats',
fraction BOOL, fraction BOOL,
maxsats INT, maxsats INT,
coins 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
);
"""
)

View File

@@ -9,7 +9,7 @@ class Cashu(BaseModel):
id: str = Query(None) id: str = Query(None)
name: str = Query(None) name: str = Query(None)
wallet: str = Query(None) wallet: str = Query(None)
tickershort: str tickershort: str = Query(None)
fraction: bool = Query(None) fraction: bool = Query(None)
maxsats: int = Query(0) maxsats: int = Query(0)
coins: int = Query(0) coins: int = Query(0)
@@ -34,6 +34,13 @@ class Pegs(BaseModel):
class PayLnurlWData(BaseModel): class PayLnurlWData(BaseModel):
lnurl: str lnurl: str
class Promises(BaseModel):
id: str
amount: int
B_b: str
C_b: str
cashu_id: str
class Proof(BaseModel): class Proof(BaseModel):
amount: int amount: int
secret: str secret: str

View File

@@ -9,7 +9,6 @@ from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_cashu from .crud import get_cashu
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue) register_invoice_listener(invoice_queue)

View File

@@ -80,13 +80,10 @@
<q-card class="q-pa-lg q-pt-xl" style="width: 500px"> <q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createMint" class="q-gutter-md"> <q-form @submit="createMint" class="q-gutter-md">
<q-input filled dense v-model.trim="formDialog.data.name" label="Mint Name" placeholder="Cashu Mint"></q-input> <q-input filled dense v-model.trim="formDialog.data.name" label="Mint Name" placeholder="Cashu Mint"></q-input>
<q-input filled dense v-model.trim="formDialog.data.tickershort" label="Ticker shorthand" placeholder="CC"
#></q-input>
<q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions" <q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions"
label="Wallet *" ></q-select> label="Wallet *" ></q-select>
<q-toggle v-model="toggleAdvanced" label="Show advanced options"></q-toggle> <q-toggle v-model="toggleAdvanced" label="Show advanced options"></q-toggle>
<div v-show="toggleAdvanced"> <div v-show="toggleAdvanced">
<div class="row"> <div class="row">
<div class="col-5"> <div class="col-5">
<q-checkbox v-model="formDialog.data.fraction" color="primary" label="sats/coins?"> <q-checkbox v-model="formDialog.data.fraction" color="primary" label="sats/coins?">
@@ -96,6 +93,8 @@
<div class="col-7"> <div class="col-7">
<q-input v-if="!formDialog.data.fraction" filled dense type="number" v-model.trim="formDialog.data.cost" label="Sat coin cost (optional)" <q-input v-if="!formDialog.data.fraction" filled dense type="number" v-model.trim="formDialog.data.cost" label="Sat coin cost (optional)"
value="1" type="number"></q-input> value="1" type="number"></q-input>
<q-input v-if="!formDialog.data.fraction" filled dense v-model.trim="formDialog.data.tickershort" label="Ticker shorthand" placeholder="CC"
#></q-input>
</div> </div>
</div> </div>
<q-input class="q-mt-md" filled dense type="number" v-model.trim="formDialog.data.maxsats" <q-input class="q-mt-md" filled dense type="number" v-model.trim="formDialog.data.maxsats"
@@ -105,7 +104,7 @@
</div> </div>
<div class="row q-mt-md"> <div class="row q-mt-md">
<q-btn unelevated color="primary" <q-btn unelevated color="primary"
:disable="formDialog.data.tickershort == null || formDialog.data.name == null" type="submit">Create Mint :disable="formDialog.data.wallet == null || formDialog.data.name == null" type="submit">Create Mint
</q-btn> </q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div> </div>

View File

@@ -22,7 +22,6 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
"cashu/index.html", {"request": request, "user": user.dict()} "cashu/index.html", {"request": request, "user": user.dict()}
) )
@cashu_ext.get("/wallet") @cashu_ext.get("/wallet")
async def cashu(request: Request): async def cashu(request: Request):
return cashu_renderer().TemplateResponse("cashu/wallet.html",{"request": request}) return cashu_renderer().TemplateResponse("cashu/wallet.html",{"request": request})

View File

@@ -15,10 +15,25 @@ from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import cashu_ext from . import cashu_ext
from .crud import create_cashu, delete_cashu, get_cashu, get_cashus, update_cashu_keys from .ledger import get_pubkeys, request_mint, mint
from .models import Cashu, Pegs, CheckPayload, MeltPayload, MintPayloads, SplitPayload, PayLnurlWData
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) @cashu_ext.get("/api/v1/cashus", status_code=HTTPStatus.OK)
async def api_cashus( async def api_cashus(
@@ -173,50 +188,54 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str):
return status return status
#################CASHU STUFF################### ########################################
#################MINT###################
########################################
@cashu_ext.get("/keys") @cashu_ext.get("/keys")
def keys(): def keys(cashu_id: str):
"""Get the public keys of the mint""" """Get the public keys of the mint"""
return ledger.get_pubkeys() return get_pubkeys(cashu_id)
@cashu_ext.get("/mint") @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.""" """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}") print(f"Lightning invoice: {payment_request}")
return {"pr": payment_request, "hash": payment_hash} return {"pr": payment_request, "hash": payment_hash}
@cashu_ext.post("/mint") @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 = [] amounts = []
B_s = [] B_s = []
for payload in payloads.blinded_messages: for payload in payloads.blinded_messages:
amounts.append(payload.amount) amounts.append(payload.amount)
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True)) B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
promises = await mint(B_s, amounts, payment_hash, cashu_id)
logger.debug(promises)
try: try:
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash) promises = await mint(B_s, amounts, payment_hash, cashu_id)
return promises return promises
except Exception as exc: except Exception as exc:
return {"error": str(exc)} return {"error": str(exc)}
@cashu_ext.post("/melt") @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} return {"paid": ok, "preimage": preimage}
@cashu_ext.post("/check") @cashu_ext.post("/check")
async def check_spendable(payload: CheckPayload): async def check_spendable_coins(payload: CheckPayload, cashu_id: str = Query(None)):
return await ledger.check_spendable(payload.proofs) return await check_spendable(payload.proofs, cashu_id)
@cashu_ext.post("/split") @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 Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split". newly minted sets with amount "split" and "total-split".
@@ -225,7 +244,7 @@ async def split(payload: SplitPayload):
amount = payload.amount amount = payload.amount
output_data = payload.output_data.blinded_messages output_data = payload.output_data.blinded_messages
try: try:
split_return = await ledger.split(proofs, amount, output_data) split_return = await split(proofs, amount, output_data)
except Exception as exc: except Exception as exc:
return {"error": str(exc)} return {"error": str(exc)}
if not split_return: if not split_return: