feat: melt tokens
This commit is contained in:
parent
c8d5a0cae4
commit
01877d3b1d
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue
Block a user