feat: melt tokens

This commit is contained in:
Vlad Stan 2022-10-07 17:44:02 +03:00
parent c8d5a0cae4
commit 01877d3b1d
5 changed files with 109 additions and 33 deletions

View File

@ -93,7 +93,9 @@ async def delete_cashu(cashu_id) -> None:
##########################################
async def store_promises(amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str):
async def store_promises(
amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str
):
for amount, B_, C_ in zip(amounts, B_s, C_s):
await store_promise(amount, B_, C_, cashu_id)
@ -113,21 +115,21 @@ async def store_promise(amount: int, B_: str, C_: str, cashu_id: str):
async def get_promises(cashu_id) -> Optional[Cashu]:
row = await db.fetchall(
"SELECT * FROM cashu.promises WHERE cashu_id = ?", (promises_id,)
"SELECT * FROM cashu.promises WHERE cashu_id = ?", (cashu_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,)
"SELECT secret from cashu.proofs_used WHERE cashu_id = ?", (cashu_id,)
)
return [row[0] for row in rows]
async def invalidate_proof(proof: Proof, cashu_id):
async def invalidate_proof(cashu_id: str, proof: Proof):
invalidate_proof_id = urlsafe_short_hash()
await (conn or db).execute(
await db.execute(
"""
INSERT INTO cashu.proofs_used
(id, amount, C, secret, cashu_id)

View File

@ -1,9 +1,16 @@
from typing import List
import math
from typing import List, Set
from lnbits import bolt11
from lnbits.core.services import check_transaction_status, fee_reserve, pay_invoice
from lnbits.extensions.cashu.models import Cashu
from lnbits.wallets.base import PaymentStatus
from .core.b_dhke import step2_bob
from .core.base import BlindedSignature
from .core.base import BlindedSignature, Proof
from .core.secp import PublicKey
from .mint_helper import derive_keys, derive_pubkeys
from .crud import get_proofs_used, invalidate_proof
from .mint_helper import derive_keys, derive_pubkeys, verify_proof
# todo: extract const
MAX_ORDER = 64
@ -39,3 +46,52 @@ async def generate_promise(master_prvkey: str, amount: int, B_: PublicKey):
secret_key = derive_keys(master_prvkey)[amount] # Get the correct key
C_ = step2_bob(B_, secret_key)
return BlindedSignature(amount=amount, C_=C_.serialize().hex())
async def melt(cashu: Cashu, proofs: List[Proof], invoice: str):
"""Invalidates proofs and pays a Lightning invoice."""
# Verify proofs
proofs_used: Set[str] = set(await get_proofs_used(cashu.id))
# if not all([verify_proof(cashu.prvkey, proofs_used, p) for p in proofs]):
# raise Exception("could not verify proofs.")
for p in proofs:
await verify_proof(cashu.prvkey, proofs_used, p)
total_provided = sum([p["amount"] for p in proofs])
invoice_obj = bolt11.decode(invoice)
amount = math.ceil(invoice_obj.amount_msat / 1000)
fees_msat = await check_fees(cashu.wallet, invoice_obj)
assert total_provided >= amount + fees_msat / 1000, Exception(
"provided proofs not enough for Lightning payment."
)
await pay_invoice(
wallet_id=cashu.wallet,
payment_request=invoice,
description=f"pay cashu invoice",
extra={"tag": "cashu", "cahsu_name": cashu.name},
)
status: PaymentStatus = await check_transaction_status(
cashu.wallet, invoice_obj.payment_hash
)
if status.paid == True:
await invalidate_proofs(cashu.id, proofs)
return status.paid, status.preimage
return False, ""
async def check_fees(wallet_id: str, decoded_invoice):
"""Returns the fees (in msat) required to pay this pr."""
amount = math.ceil(decoded_invoice.amount_msat / 1000)
status: PaymentStatus = await check_transaction_status(
wallet_id, decoded_invoice.payment_hash
)
fees_msat = fee_reserve(amount * 1000) if status.paid != True else 0
return fees_msat
async def invalidate_proofs(cashu_id: str, proofs: List[Proof]):
"""Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
for p in proofs:
await invalidate_proof(cashu_id, p)

View File

@ -1,7 +1,9 @@
import hashlib
from typing import List
from typing import List, Set
from .core.secp import PrivateKey
from .core.b_dhke import verify
from .core.secp import PrivateKey, PublicKey
from .models import Proof
# todo: extract const
MAX_ORDER = 64
@ -21,6 +23,19 @@ def derive_keys(master_key: str):
}
def derive_pubkeys(keys: List[PrivateKey]):
return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
async def verify_proof(master_prvkey: str, proofs_used: Set[str], proof: Proof):
"""Verifies that the proof of promise was issued by this ledger."""
if proof.secret in proofs_used:
raise Exception(f"tokens already spent. Secret: {proof.secret}")
secret_key = derive_keys(master_prvkey)[
proof.amount
] # Get the correct key to check against
C = PublicKey(bytes.fromhex(proof.C), raw=True)
validMintSig = verify(secret_key, C, proof.secret)
if validMintSig != True:
raise Exception(f"tokens not valid. Secret: {proof.secret}")

View File

@ -146,9 +146,3 @@ class MeltPayload(BaseModel):
proofs: List[Proof]
amount: int
invoice: str
class CreateTokens(BaseModel):
# cashu_id: str = Query(None)
payloads: MintPayloads
payment_hash: Union[str, None] = None

View File

@ -28,11 +28,10 @@ from .crud import (
update_lightning_invoice,
)
from .ledger import mint, request_mint
from .mint import generate_promises, get_pubkeys
from .mint import generate_promises, get_pubkeys, melt
from .models import (
Cashu,
CheckPayload,
CreateTokens,
Invoice,
MeltPayload,
MintPayloads,
@ -248,7 +247,7 @@ async def mint_pay_request(
except Exception as e:
logger.error(e)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(cashu_id)
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
return {"pr": payment_request, "hash": payment_hash}
@ -265,7 +264,6 @@ async def mint_coins(
Requests the minting of tokens belonging to a paid payment request.
Call this endpoint after `GET /mint`.
"""
print("############################ amount")
cashu: Cashu = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
@ -282,12 +280,11 @@ async def mint_coins(
)
if invoice.issued == True:
raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Tokens already issued for this invoice."
status_code=HTTPStatus.PAYMENT_REQUIRED,
detail="Tokens already issued for this invoice.",
)
status: PaymentStatus = await check_transaction_status(
cashu.wallet, payment_hash
)
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
# todo: revert to: status.paid != True:
if status.paid == False:
raise HTTPException(
@ -302,21 +299,33 @@ async def mint_coins(
amounts.append(payload.amount)
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
promises = await generate_promises(cashu.prvkey, amounts, B_s)
for amount, B_, p in zip(amounts, B_s, promises):
await store_promise(amount, B_.serialize().hex(), p.C_, cashu_id)
return promises
except Exception as exc:
return CashuError(error=str(exc))
except Exception as e:
logger.error(e)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
@cashu_ext.post("/melt")
@cashu_ext.post("/api/v1/melt/{cashu_id}")
async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)):
ok, preimage = await melt(payload.proofs, payload.amount, payload.invoice, cashu_id)
return {"paid": ok, "preimage": preimage}
cashu: Cashu = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
try:
ok, preimage = await melt(cashu, payload.proofs, payload.invoice)
return {"paid": ok, "preimage": preimage}
except Exception as e:
logger.error(e)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)
)
@cashu_ext.post("/check")