diff --git a/lnbits/extensions/cashu/core/secp.py b/lnbits/extensions/cashu/core/secp.py new file mode 100644 index 00000000..33416434 --- /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 00000000..703bf426 --- /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 00000000..30e66b03 --- /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 391aeda1..65323715 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")