diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py index 6e19eb6e6..440be0923 100644 --- a/lnbits/extensions/cashu/__init__.py +++ b/lnbits/extensions/cashu/__init__.py @@ -18,7 +18,6 @@ cashu_static_files = [ "name": "cashu_static", } ] -sys.path.append("/Users/cc/git/cashu") from cashu.mint.ledger import Ledger ledger = Ledger( @@ -28,7 +27,7 @@ ledger = Ledger( derivation_path="0/0/0/1", ) -cashu_ext: APIRouter = APIRouter(prefix="/api/v1/cashu", tags=["cashu"]) +cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"]) def cashu_renderer(): diff --git a/lnbits/extensions/cashu/core/b_dhke.py b/lnbits/extensions/cashu/core/b_dhke.py deleted file mode 100644 index ff0bc5157..000000000 --- a/lnbits/extensions/cashu/core/b_dhke.py +++ /dev/null @@ -1,88 +0,0 @@ -# Don't trust me with cryptography. - -""" -Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406 -Alice: -A = a*G -return A -Bob: -Y = hash_to_curve(secret_message) -r = random blinding factor -B'= Y + r*G -return B' -Alice: -C' = a*B' - (= a*Y + a*r*G) -return C' -Bob: -C = C' - r*A - (= C' - a*r*G) - (= a*Y) -return C, secret_message -Alice: -Y = hash_to_curve(secret_message) -C == a*Y -If true, C must have originated from Alice -""" - -import hashlib - -from secp256k1 import PrivateKey, PublicKey - - -def hash_to_curve(message: bytes): - """Generates a point 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 - msg_to_hash = message - while point is None: - try: - _hash = hashlib.sha256(msg_to_hash).digest() - point = PublicKey(b"\x02" + _hash, raw=True) - except: - msg_to_hash = _hash - return point - - -def step1_alice(secret_msg): - secret_msg = secret_msg - Y = hash_to_curve(secret_msg) - r = PrivateKey() - B_ = Y + r.pubkey - return B_, r - - -def step2_bob(B_, a): - C_ = B_.mult(a) - return C_ - - -def step3_alice(C_, r, A): - C = C_ - A.mult(r) - return C - - -def verify(a, C, secret_msg): - Y = hash_to_curve(secret_msg) - return C == Y.mult(a) - - -### Below is a test of a simple positive and negative case - -# # Alice's keys -# a = PrivateKey() -# A = a.pubkey -# secret_msg = "test" -# B_, r = step1_alice(secret_msg) -# C_ = step2_bob(B_, a) -# C = step3_alice(C_, r, A) -# print("C:{}, secret_msg:{}".format(C, secret_msg)) -# assert verify(a, C, secret_msg) -# assert verify(a, C + C, secret_msg) == False # adding C twice shouldn't pass -# assert verify(a, A, secret_msg) == False # A shouldn't pass - -# # Test operations -# b = PrivateKey() -# B = b.pubkey -# assert -A -A + A == -A # neg -# assert B.mult(a) == A.mult(b) # a*B = A*b diff --git a/lnbits/extensions/cashu/core/base.py b/lnbits/extensions/cashu/core/base.py deleted file mode 100644 index 947da9871..000000000 --- a/lnbits/extensions/cashu/core/base.py +++ /dev/null @@ -1,168 +0,0 @@ -from sqlite3 import Row -from typing import List, Union - -from pydantic import BaseModel - - -class CashuError(BaseException): - 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( - cashu_id=str(row[0]), - amount=int(row[1]), - pr=str(row[2]), - hash=str(row[3]), - issued=bool(row[4]), - ) - - -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/core/secp.py b/lnbits/extensions/cashu/core/secp.py deleted file mode 100644 index 334164344..000000000 --- a/lnbits/extensions/cashu/core/secp.py +++ /dev/null @@ -1,52 +0,0 @@ -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/core/split.py b/lnbits/extensions/cashu/core/split.py deleted file mode 100644 index 44b9cf51d..000000000 --- a/lnbits/extensions/cashu/core/split.py +++ /dev/null @@ -1,8 +0,0 @@ -def amount_split(amount): - """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" - bits_amt = bin(amount)[::-1][:-2] - rv = [] - for (pos, bit) in enumerate(bits_amt): - if bit == "1": - rv.append(2**pos) - return rv diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 6315a765e..b271bccc7 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -64,7 +64,7 @@ LIGHTNING = False ######################################## # todo: use /mints -@cashu_ext.get("/cashus", status_code=HTTPStatus.OK) +@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) ): @@ -75,7 +75,7 @@ async def api_cashus( return [cashu.dict() for cashu in await get_cashus(wallet_ids)] -@cashu_ext.post("/cashus", status_code=HTTPStatus.CREATED) +@cashu_ext.post("/api/v1/cashus", status_code=HTTPStatus.CREATED) async def api_cashu_create(data: Cashu, wallet: WalletTypeInfo = Depends(get_key_type)): cashu_id = urlsafe_short_hash() # generate a new keyset in cashu @@ -93,7 +93,7 @@ async def api_cashu_create(data: Cashu, wallet: WalletTypeInfo = Depends(get_key ####################################### -@cashu_ext.get("/{cashu_id}/keys", status_code=HTTPStatus.OK) +@cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK) async def keys(cashu_id: str = Query(None)) -> dict[int, str]: """Get the public keys of the mint""" cashu: Union[Cashu, None] = await get_cashu(cashu_id) @@ -106,7 +106,7 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]: return ledger.get_keyset(keyset_id=cashu.keyset_id) -@cashu_ext.get("/{cashu_id}/mint") +@cashu_ext.get("/api/v1/cashu/{cashu_id}/mint") async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse: """ Request minting of new tokens. The mint responds with a Lightning invoice. @@ -144,7 +144,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR return resp -@cashu_ext.post("/{cashu_id}/mint") +@cashu_ext.post("/api/v1/cashu/{cashu_id}/mint") async def mint_coins( data: MintRequest, cashu_id: str = Query(None), @@ -203,7 +203,7 @@ async def mint_coins( raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) -@cashu_ext.post("/{cashu_id}/melt") +@cashu_ext.post("/api/v1/cashu/{cashu_id}/melt") async def melt_coins( payload: MeltRequest, cashu_id: str = Query(None) ) -> GetMeltResponse: @@ -258,13 +258,13 @@ async def melt_coins( return GetMeltResponse(paid=status.paid, preimage=status.preimage) -@cashu_ext.post("/check") +@cashu_ext.post("/api/v1/check") async def check_spendable(payload: CheckRequest) -> Dict[int, bool]: """Check whether a secret has been spent already or not.""" return await ledger.check_spendable(payload.proofs) -@cashu_ext.post("/checkfees") +@cashu_ext.post("/api/v1/checkfees") async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: """ Responds with the fees necessary to pay a Lightning invoice. @@ -281,7 +281,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: return CheckFeesResponse(fee=fees_msat / 1000) -@cashu_ext.post("/{cashu_id}/split") +@cashu_ext.post("/api/v1/cashu/{cashu_id}/split") async def split( payload: SplitRequest, cashu_id: str = Query(None) ) -> PostSplitResponse: @@ -299,6 +299,7 @@ async def split( outputs = payload.outputs.blinded_messages # backwards compatibility with clients < v0.2.2 assert outputs, Exception("no outputs provided.") + split_return = None try: split_return = await ledger.split(proofs, amount, outputs, cashu.keyset_id) except Exception as exc: @@ -316,7 +317,7 @@ async def split( return resp -# @cashu_ext.post("/api/v1/cashus/upodatekeys", status_code=HTTPStatus.CREATED) +# @cashu_ext.post("/api/v1s/upodatekeys", status_code=HTTPStatus.CREATED) # async def api_cashu_update_keys( # data: Cashu, wallet: WalletTypeInfo = Depends(get_key_type) # ): @@ -327,7 +328,7 @@ async def split( # return cashu.dict() -# @cashu_ext.delete("/api/v1/cashus/{cashu_id}") +# @cashu_ext.delete("/api/v1s/{cashu_id}") # async def api_cashu_delete( # cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # ): @@ -348,7 +349,7 @@ async def split( # ######################################## # #################????################### # ######################################## -# @cashu_ext.post("/api/v1/cashus/{cashu_id}/invoices", status_code=HTTPStatus.CREATED) +# @cashu_ext.post("/api/v1s/{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 # ): @@ -376,7 +377,7 @@ async def split( # @cashu_ext.post( -# "/api/v1/cashus/{cashu_id}/invoices/{payment_request}/pay", +# "/api/v1s/{cashu_id}/invoices/{payment_request}/pay", # status_code=HTTPStatus.OK, # ) # async def api_cashu_pay_invoice( @@ -437,7 +438,7 @@ async def split( # @cashu_ext.get( -# "/api/v1/cashus/{cashu_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK +# "/api/v1s/{cashu_id}/invoices/{payment_hash}", status_code=HTTPStatus.OK # ) # async def api_cashu_check_invoice(cashu_id: str, payment_hash: str): # cashu = await get_cashu(cashu_id) @@ -459,7 +460,7 @@ async def split( # ######################################## -# # @cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK) +# # @cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK) # # async def keys(cashu_id: str = Query(False)): # # """Get the public keys of the mint""" # # mint = await get_cashu(cashu_id) @@ -470,7 +471,7 @@ async def split( # # return get_pubkeys(mint.prvkey) -# @cashu_ext.get("/api/v1/cashu/{cashu_id}/mint") +# @cashu_ext.get("/api/v1/{cashu_id}/mint") # async def mint_pay_request(amount: int = 0, cashu_id: str = Query(None)): # """Request minting of tokens. Server responds with a Lightning invoice.""" @@ -498,7 +499,7 @@ async def split( # return {"pr": payment_request, "hash": payment_hash} -# @cashu_ext.post("/api/v1/cashu/{cashu_id}/mint") +# @cashu_ext.post("/api/v1/{cashu_id}/mint") # async def mint_coins( # data: MintPayloads, # cashu_id: str = Query(None), @@ -560,7 +561,7 @@ async def split( # raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) -# @cashu_ext.post("/api/v1/cashu/{cashu_id}/melt") +# @cashu_ext.post("/api/v1/{cashu_id}/melt") # async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)): # """Invalidates proofs and pays a Lightning invoice.""" # cashu: Cashu = await get_cashu(cashu_id) @@ -576,12 +577,12 @@ async def split( # raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) -# @cashu_ext.post("/api/v1/cashu/{cashu_id}/check") +# @cashu_ext.post("/api/v1/{cashu_id}/check") # async def check_spendable_coins(payload: CheckPayload, cashu_id: str = Query(None)): # return await check_spendable(payload.proofs, cashu_id) -# @cashu_ext.post("/api/v1/cashu/{cashu_id}/split") +# @cashu_ext.post("/api/v1/{cashu_id}/split") # async def split_proofs(payload: SplitRequest, cashu_id: str = Query(None)): # """ # Requetst a set of tokens with amount "total" to be split into two