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):
|
for amount, B_, C_ in zip(amounts, B_s, C_s):
|
||||||
await store_promise(amount, B_, C_, cashu_id)
|
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]:
|
async def get_promises(cashu_id) -> Optional[Cashu]:
|
||||||
row = await db.fetchall(
|
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
|
return Promises(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_proofs_used(cashu_id):
|
async def get_proofs_used(cashu_id):
|
||||||
rows = await db.fetchall(
|
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]
|
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()
|
invalidate_proof_id = urlsafe_short_hash()
|
||||||
await (conn or db).execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO cashu.proofs_used
|
INSERT INTO cashu.proofs_used
|
||||||
(id, amount, C, secret, cashu_id)
|
(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.b_dhke import step2_bob
|
||||||
from .core.base import BlindedSignature
|
from .core.base import BlindedSignature, Proof
|
||||||
from .core.secp import PublicKey
|
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
|
# todo: extract const
|
||||||
MAX_ORDER = 64
|
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
|
secret_key = derive_keys(master_prvkey)[amount] # Get the correct key
|
||||||
C_ = step2_bob(B_, secret_key)
|
C_ = step2_bob(B_, secret_key)
|
||||||
return BlindedSignature(amount=amount, C_=C_.serialize().hex())
|
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
|
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
|
# todo: extract const
|
||||||
MAX_ORDER = 64
|
MAX_ORDER = 64
|
||||||
|
@ -21,6 +23,19 @@ def derive_keys(master_key: str):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def derive_pubkeys(keys: List[PrivateKey]):
|
def derive_pubkeys(keys: List[PrivateKey]):
|
||||||
return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
|
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]
|
proofs: List[Proof]
|
||||||
amount: int
|
amount: int
|
||||||
invoice: str
|
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,
|
update_lightning_invoice,
|
||||||
)
|
)
|
||||||
from .ledger import mint, request_mint
|
from .ledger import mint, request_mint
|
||||||
from .mint import generate_promises, get_pubkeys
|
from .mint import generate_promises, get_pubkeys, melt
|
||||||
from .models import (
|
from .models import (
|
||||||
Cashu,
|
Cashu,
|
||||||
CheckPayload,
|
CheckPayload,
|
||||||
CreateTokens,
|
|
||||||
Invoice,
|
Invoice,
|
||||||
MeltPayload,
|
MeltPayload,
|
||||||
MintPayloads,
|
MintPayloads,
|
||||||
|
@ -248,7 +247,7 @@ async def mint_pay_request(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
raise HTTPException(
|
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}
|
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.
|
Requests the minting of tokens belonging to a paid payment request.
|
||||||
Call this endpoint after `GET /mint`.
|
Call this endpoint after `GET /mint`.
|
||||||
"""
|
"""
|
||||||
print("############################ amount")
|
|
||||||
cashu: Cashu = await get_cashu(cashu_id)
|
cashu: Cashu = await get_cashu(cashu_id)
|
||||||
if cashu is None:
|
if cashu is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -282,12 +280,11 @@ async def mint_coins(
|
||||||
)
|
)
|
||||||
if invoice.issued == True:
|
if invoice.issued == True:
|
||||||
raise HTTPException(
|
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(
|
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
||||||
cashu.wallet, payment_hash
|
|
||||||
)
|
|
||||||
# todo: revert to: status.paid != True:
|
# todo: revert to: status.paid != True:
|
||||||
if status.paid == False:
|
if status.paid == False:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -302,21 +299,33 @@ async def mint_coins(
|
||||||
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 generate_promises(cashu.prvkey, amounts, B_s)
|
promises = await generate_promises(cashu.prvkey, amounts, B_s)
|
||||||
for amount, B_, p in zip(amounts, B_s, promises):
|
for amount, B_, p in zip(amounts, B_s, promises):
|
||||||
await store_promise(amount, B_.serialize().hex(), p.C_, cashu_id)
|
await store_promise(amount, B_.serialize().hex(), p.C_, cashu_id)
|
||||||
|
|
||||||
return promises
|
return promises
|
||||||
except Exception as exc:
|
except Exception as e:
|
||||||
return CashuError(error=str(exc))
|
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)):
|
async def melt_coins(payload: MeltPayload, cashu_id: str = Query(None)):
|
||||||
|
cashu: Cashu = await get_cashu(cashu_id)
|
||||||
ok, preimage = await melt(payload.proofs, payload.amount, payload.invoice, 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}
|
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")
|
@cashu_ext.post("/check")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user