This commit is contained in:
callebtc 2022-10-14 01:01:43 +02:00 committed by dni ⚡
parent 5b1ee554df
commit 53db275fe0
7 changed files with 141 additions and 617 deletions

View File

@ -11,13 +11,16 @@ db = Database("ext_cashu")
import sys import sys
cashu_static_files = [
{
"path": "/cashu/static",
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
"name": "cashu_static",
}
]
sys.path.append("/Users/cc/git/cashu") sys.path.append("/Users/cc/git/cashu")
from cashu.mint.ledger import Ledger from cashu.mint.ledger import Ledger
# from .crud import LedgerCrud
# db = Database("ext_cashu", LNBITS_DATA_FOLDER)
ledger = Ledger( ledger = Ledger(
db=db, db=db,
# seed=MINT_PRIVATE_KEY, # seed=MINT_PRIVATE_KEY,
@ -26,17 +29,6 @@ ledger = Ledger(
) )
cashu_ext: APIRouter = APIRouter(prefix="/api/v1/cashu", tags=["cashu"]) cashu_ext: APIRouter = APIRouter(prefix="/api/v1/cashu", tags=["cashu"])
# from cashu.mint.router import router as cashu_router
# cashu_ext.include_router(router=cashu_router)
cashu_static_files = [
{
"path": "/cashu/static",
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
"name": "cashu_static",
}
]
def cashu_renderer(): def cashu_renderer():

View File

@ -19,46 +19,6 @@ from cashu.core.base import MintKeyset
from lnbits.db import Database, Connection from lnbits.db import Database, Connection
# class LedgerCrud:
# """
# Database interface for Cashu mint.
# This class needs to be overloaded by any app that imports the Cashu mint.
# """
# async def get_keyset(*args, **kwags):
# return await get_keyset(*args, **kwags)
# async def get_lightning_invoice(*args, **kwags):
# return await get_lightning_invoice(*args, **kwags)
# async def get_proofs_used(*args, **kwags):
# return await get_proofs_used(*args, **kwags)
# async def invalidate_proof(*args, **kwags):
# return await invalidate_proof(*args, **kwags)
# async def store_keyset(*args, **kwags):
# return await store_keyset(*args, **kwags)
# async def store_lightning_invoice(*args, **kwags):
# return await store_lightning_invoice(*args, **kwags)
# async def store_promise(*args, **kwags):
# return await store_promise(*args, **kwags)
# async def update_lightning_invoice(*args, **kwags):
# return await update_lightning_invoice(*args, **kwags)
async def create_cashu( async def create_cashu(
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
) -> Cashu: ) -> Cashu:
@ -85,23 +45,23 @@ async def create_cashu(
return cashu return cashu
async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]: # async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]:
entropy = bytes([random.getrandbits(8) for i in range(16)]) # entropy = bytes([random.getrandbits(8) for i in range(16)])
mnemonic = bip39.mnemonic_from_bytes(entropy) # mnemonic = bip39.mnemonic_from_bytes(entropy)
seed = bip39.mnemonic_to_seed(mnemonic) # seed = bip39.mnemonic_to_seed(mnemonic)
root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"]) # root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
bip44_xprv = root.derive("m/44h/1h/0h") # bip44_xprv = root.derive("m/44h/1h/0h")
bip44_xpub = bip44_xprv.to_public() # bip44_xpub = bip44_xprv.to_public()
await db.execute( # await db.execute(
"UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?", # "UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?",
bip44_xprv.to_base58(), # bip44_xprv.to_base58(),
bip44_xpub.to_base58(), # bip44_xpub.to_base58(),
cashu_id, # cashu_id,
) # )
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) # row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
return Cashu(**row) if row else None # return Cashu(**row) if row else None
async def get_cashu(cashu_id) -> Optional[Cashu]: async def get_cashu(cashu_id) -> Optional[Cashu]:
@ -130,103 +90,103 @@ async def delete_cashu(cashu_id) -> None:
# ########################################## # ##########################################
async def store_promises( # async def store_promises(
amounts: List[int], B_s: List[str], C_s: List[str], cashu_id: str # 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)
async def store_promise(amount: int, B_: str, C_: str, cashu_id: str): # async def store_promise(amount: int, B_: str, C_: str, cashu_id: str):
promise_id = urlsafe_short_hash() # promise_id = urlsafe_short_hash()
await db.execute( # await db.execute(
""" # """
INSERT INTO cashu.promises # INSERT INTO cashu.promises
(id, amount, B_b, C_b, cashu_id) # (id, amount, B_b, C_b, cashu_id)
VALUES (?, ?, ?, ?, ?) # VALUES (?, ?, ?, ?, ?)
""", # """,
(promise_id, amount, str(B_), str(C_), cashu_id), # (promise_id, amount, str(B_), str(C_), cashu_id),
) # )
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 = ?", (cashu_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( # async def get_proofs_used(
db: Database, # db: Database,
conn: Optional[Connection] = None, # conn: Optional[Connection] = None,
): # ):
rows = await (conn or db).fetchall( # rows = await (conn or db).fetchall(
""" # """
SELECT secret from cashu.proofs_used # SELECT secret from cashu.proofs_used
""" # """
) # )
return [row[0] for row in rows] # return [row[0] for row in rows]
async def invalidate_proof(cashu_id: str, proof: Proof): # async def invalidate_proof(cashu_id: str, proof: Proof):
invalidate_proof_id = urlsafe_short_hash() # invalidate_proof_id = urlsafe_short_hash()
await 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)
VALUES (?, ?, ?, ?, ?) # VALUES (?, ?, ?, ?, ?)
""", # """,
(invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), cashu_id), # (invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), cashu_id),
) # )
######################################## # ########################################
############ MINT INVOICES ############# # ############ MINT INVOICES #############
######################################## # ########################################
async def store_lightning_invoice(cashu_id: str, invoice: Invoice): # async def store_lightning_invoice(cashu_id: str, invoice: Invoice):
await db.execute( # await db.execute(
""" # """
INSERT INTO cashu.invoices # INSERT INTO cashu.invoices
(cashu_id, amount, pr, hash, issued) # (cashu_id, amount, pr, hash, issued)
VALUES (?, ?, ?, ?, ?) # VALUES (?, ?, ?, ?, ?)
""", # """,
( # (
cashu_id, # cashu_id,
invoice.amount, # invoice.amount,
invoice.pr, # invoice.pr,
invoice.hash, # invoice.hash,
invoice.issued, # invoice.issued,
), # ),
) # )
async def get_lightning_invoice(cashu_id: str, hash: str): # async def get_lightning_invoice(cashu_id: str, hash: str):
row = await db.fetchone( # row = await db.fetchone(
""" # """
SELECT * from cashu.invoices # SELECT * from cashu.invoices
WHERE cashu_id =? AND hash = ? # WHERE cashu_id =? AND hash = ?
""", # """,
( # (
cashu_id, # cashu_id,
hash, # hash,
), # ),
) # )
return Invoice.from_row(row) # return Invoice.from_row(row)
async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool): # async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool):
await db.execute( # await db.execute(
"UPDATE cashu.invoices SET issued = ? WHERE cashu_id = ? AND hash = ?", # "UPDATE cashu.invoices SET issued = ? WHERE cashu_id = ? AND hash = ?",
( # (
issued, # issued,
cashu_id, # cashu_id,
hash, # hash,
), # ),
) # )
############################## ##############################

View File

@ -30,118 +30,3 @@ async def m001_initial(db):
); );
""" """
) )
# async def m001_initial(db):
# await db.execute(
# """
# CREATE TABLE IF NOT EXISTS cashu.promises (
# amount INTEGER NOT NULL,
# B_b TEXT NOT NULL,
# C_b TEXT NOT NULL,
# UNIQUE (B_b)
# );
# """
# )
# await db.execute(
# """
# CREATE TABLE IF NOT EXISTS cashu.proofs_used (
# amount INTEGER NOT NULL,
# C TEXT NOT NULL,
# secret TEXT NOT NULL,
# UNIQUE (secret)
# );
# """
# )
# await db.execute(
# """
# CREATE TABLE IF NOT EXISTS cashu.invoices (
# amount INTEGER NOT NULL,
# pr TEXT NOT NULL,
# hash TEXT NOT NULL,
# issued BOOL NOT NULL,
# UNIQUE (hash)
# );
# """
# )
# await db.execute(
# """
# CREATE VIEW IF NOT EXISTS cashu.balance_issued AS
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
# SELECT SUM(amount) AS s
# FROM cashu.promises
# WHERE amount > 0
# );
# """
# )
# await db.execute(
# """
# CREATE VIEW IF NOT EXISTS cashu.balance_used AS
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
# SELECT SUM(amount) AS s
# FROM cashu.proofs_used
# WHERE amount > 0
# );
# """
# )
# await db.execute(
# """
# CREATE VIEW IF NOT EXISTS cashu.balance AS
# SELECT s_issued - s_used AS balance FROM (
# SELECT bi.balance AS s_issued, bu.balance AS s_used
# FROM cashu.balance_issued bi
# CROSS JOIN balance_used bu
# );
# """
# )
# async def m003_mint_keysets(db):
# """
# Stores mint keysets from different mints and epochs.
# """
# await db.execute(
# f"""
# CREATE TABLE IF NOT EXISTS cashu.keysets (
# id TEXT NOT NULL,
# derivation_path TEXT,
# valid_from TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
# valid_to TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
# first_seen TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
# active BOOL DEFAULT TRUE,
# UNIQUE (derivation_path)
# );
# """
# )
# await db.execute(
# f"""
# CREATE TABLE IF NOT EXISTS cashu.mint_pubkeys (
# id TEXT NOT NULL,
# amount INTEGER NOT NULL,
# pubkey TEXT NOT NULL,
# UNIQUE (id, pubkey)
# );
# """
# )
# async def m004_keysets_add_version(db):
# """
# Column that remembers with which version
# """
# await db.execute("ALTER TABLE cashu.keysets ADD COLUMN version TEXT")

View File

@ -1,155 +0,0 @@
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.wallets.base import PaymentStatus
from .core.b_dhke import step2_bob
from .core.base import BlindedMessage, BlindedSignature, Proof
from .core.secp import PublicKey
from .core.split import amount_split
from .crud import get_proofs_used, invalidate_proof
from .mint_helper import (
derive_keys,
derive_pubkeys,
verify_equation_balanced,
verify_no_duplicates,
verify_outputs,
verify_proof,
verify_secret_criteria,
verify_split_amount,
)
from .models import Cashu
# todo: extract const
MAX_ORDER = 64
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()}
async def generate_promises(
master_prvkey: str, amounts: List[int], B_s: List[PublicKey]
):
"""Mints a promise for coins for B_."""
for amount in amounts:
if amount not in [2**i for i in range(MAX_ORDER)]:
raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.")
promises = [
await generate_promise(master_prvkey, amount, B_)
for B_, amount in zip(B_s, amounts)
]
return promises
async def generate_promise(master_prvkey: str, amount: int, B_: PublicKey):
"""Generates a promise for given amount and returns a pair (amount, C')."""
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))
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(
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
)
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 split(
cashu: Cashu, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
):
"""Consumes proofs and prepares new promises based on the amount split."""
total = sum([p.amount for p in proofs])
# verify that amount is kosher
verify_split_amount(amount)
# verify overspending attempt
if amount > total:
raise Exception(
f"split amount ({amount}) is higher than the total sum ({total})."
)
# Verify secret criteria
if not all([verify_secret_criteria(p) for p in proofs]):
raise Exception("secrets do not match criteria.")
# verify that only unique proofs and outputs were used
if not verify_no_duplicates(proofs, outputs):
raise Exception("duplicate proofs or promises.")
# verify that outputs have the correct amount
if not verify_outputs(total, amount, outputs): # ?
raise Exception("split of promises is not as expected.")
# Verify proofs
# Verify proofs
proofs_used: Set[str] = set(await get_proofs_used(cashu.id))
for p in proofs:
await verify_proof(cashu.prvkey, proofs_used, p)
# Mark proofs as used and prepare new promises
await invalidate_proofs(cashu.id, proofs)
outs_fst = amount_split(total - amount)
outs_snd = amount_split(amount)
B_fst = [
PublicKey(bytes.fromhex(od.B_), raw=True) for od in outputs[: len(outs_fst)]
]
B_snd = [
PublicKey(bytes.fromhex(od.B_), raw=True) for od in outputs[len(outs_fst) :]
]
# PublicKey(bytes.fromhex(payload.B_), raw=True)
prom_fst, prom_snd = await generate_promises(
cashu.prvkey, outs_fst, B_fst
), await generate_promises(cashu.prvkey, outs_snd, B_snd)
# verify amounts in produced proofs
verify_equation_balanced(proofs, prom_fst + prom_snd)
return prom_fst, prom_snd
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,97 +0,0 @@
import base64
import hashlib
from typing import List, Set
from .core.b_dhke import verify
from .core.base import BlindedSignature
from .core.secp import PrivateKey, PublicKey
from .core.split import amount_split
from .models import BlindedMessage, Proof
# 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)]}
# async required?
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)
secret = base64.standard_b64decode(proof.secret)
print("### secret", secret)
validMintSig = verify(secret_key, C, secret)
if validMintSig != True:
raise Exception(f"tokens not valid. Secret: {proof.secret}")
def verify_split_amount(amount: int):
"""Split amount like output amount can't be negative or too big."""
try:
verify_amount(amount)
except:
# For better error message
raise Exception("invalid split amount: " + str(amount))
def verify_secret_criteria(proof: Proof):
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
return True
def verify_no_duplicates(proofs: List[Proof], outputs: List[BlindedMessage]):
secrets = [p.secret for p in proofs]
if len(secrets) != len(list(set(secrets))):
return False
B_s = [od.B_ for od in outputs]
if len(B_s) != len(list(set(B_s))):
return False
return True
def verify_outputs(total: int, amount: int, outputs: List[BlindedMessage]):
"""Verifies the expected split was correctly computed"""
frst_amt, scnd_amt = total - amount, amount # we have two amounts to split to
frst_outputs = amount_split(frst_amt)
scnd_outputs = amount_split(scnd_amt)
expected = frst_outputs + scnd_outputs
given = [o.amount for o in outputs]
return given == expected
def verify_amount(amount: int):
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER
if not valid:
raise Exception("invalid amount: " + str(amount))
return amount
def verify_equation_balanced(proofs: List[Proof], outs: List[BlindedSignature]):
"""Verify that Σoutputs - Σinputs = 0."""
sum_inputs = sum(verify_amount(p.amount) for p in proofs)
sum_outputs = sum(verify_amount(p.amount) for p in outs)
assert sum_outputs - sum_inputs == 0

View File

@ -1,17 +1,11 @@
import asyncio import asyncio
import json import json
from lnbits.core import db as core_db
from lnbits.core.crud import create_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import urlsafe_short_hash from lnbits.tasks import register_invoice_listener
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_cashu from .crud import get_cashu
import sys
sys.path.append("/Users/cc/git/cashu")
from cashu.mint import migrations from cashu.mint import migrations
from cashu.core.migrations import migrate_databases from cashu.core.migrations import migrate_databases
from . import db, ledger from . import db, ledger
@ -35,51 +29,6 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") == "cashu" and payment.extra.get("tipSplitted"): if not payment.extra.get("tag") == "cashu":
# already splitted, ignore
return return
# now we make some special internal transfers (from no one to the receiver)
cashu = await get_cashu(payment.extra.get("cashuId"))
tipAmount = payment.extra.get("tipAmount")
if tipAmount is None:
# no tip amount
return
tipAmount = tipAmount * 1000
amount = payment.amount - tipAmount
# mark the original payment with one extra key, "splitted"
# (this prevents us from doing this process again and it's informative)
# and reduce it by the amount we're going to send to the producer
await core_db.execute(
"""
UPDATE apipayments
SET extra = ?, amount = ?
WHERE hash = ?
AND checking_id NOT LIKE 'internal_%'
""",
(
json.dumps(dict(**payment.extra, tipSplitted=True)),
amount,
payment.payment_hash,
),
)
# perform the internal transfer using the same payment_hash
internal_checking_id = f"internal_{urlsafe_short_hash()}"
await create_payment(
wallet_id=cashu.tip_wallet,
checking_id=internal_checking_id,
payment_request="",
payment_hash=payment.payment_hash,
amount=tipAmount,
memo=f"Tip for {payment.memo}",
pending=False,
extra={"tipSplitted": True},
)
# manually send this for now
await internal_invoice_queue.put(internal_checking_id)
return return

View File

@ -2,6 +2,7 @@ import json
from http import HTTPStatus from http import HTTPStatus
from typing import Union from typing import Union
import math import math
from typing import Dict, List, Union
import httpx import httpx
from fastapi import Query from fastapi import Query
@ -25,38 +26,22 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.wallets.base import PaymentStatus from lnbits.wallets.base import PaymentStatus
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from lnbits.core.crud import check_internal from lnbits.core.crud import check_internal
# --------- extension imports
from . import cashu_ext from . import cashu_ext
from .core.base import CashuError, PostSplitResponse, SplitRequest
from .crud import ( from .crud import (
create_cashu, create_cashu,
delete_cashu, delete_cashu,
get_cashu, get_cashu,
get_cashus, get_cashus,
get_lightning_invoice,
store_lightning_invoice,
store_promise,
update_lightning_invoice,
) )
# from .ledger import mint, request_mint from .models import Cashu
from .mint import generate_promises, get_pubkeys, melt, split
from .models import (
Cashu,
CheckPayload,
Invoice,
MeltPayload,
MintPayloads,
PayLnurlWData,
Pegs,
SplitPayload,
)
from . import ledger
############### IMPORT CALLE # -------- cashu imports
from typing import Dict, List, Union
from secp256k1 import PublicKey
from cashu.core.base import ( from cashu.core.base import (
Proof, Proof,
BlindedSignature, BlindedSignature,
@ -69,9 +54,8 @@ from cashu.core.base import (
MintRequest, MintRequest,
PostSplitResponse, PostSplitResponse,
SplitRequest, SplitRequest,
Invoice,
) )
from cashu.core.errors import CashuError
from . import db, ledger
LIGHTNING = False LIGHTNING = False
@ -165,7 +149,7 @@ async def mint_coins(
data: MintRequest, data: MintRequest,
cashu_id: str = Query(None), cashu_id: str = Query(None),
payment_hash: str = Query(None), payment_hash: str = Query(None),
): ) -> List[BlindedSignature]:
""" """
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`.
@ -220,7 +204,9 @@ async def mint_coins(
@cashu_ext.post("/{cashu_id}/melt") @cashu_ext.post("/{cashu_id}/melt")
async def melt_coins(payload: MeltRequest, cashu_id: str = Query(None)): async def melt_coins(
payload: MeltRequest, cashu_id: str = Query(None)
) -> GetMeltResponse:
"""Invalidates proofs and pays a Lightning invoice.""" """Invalidates proofs and pays a Lightning invoice."""
cashu: Union[None, Cashu] = await get_cashu(cashu_id) cashu: Union[None, Cashu] = await get_cashu(cashu_id)
if cashu is None: if cashu is None:
@ -229,8 +215,6 @@ async def melt_coins(payload: MeltRequest, cashu_id: str = Query(None)):
) )
proofs = payload.proofs proofs = payload.proofs
invoice = payload.invoice invoice = payload.invoice
# async def melt(cashu: Cashu, proofs: List[Proof], invoice: str):
# """Invalidates proofs and pays a Lightning invoice."""
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID # !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID # THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
@ -271,18 +255,7 @@ async def melt_coins(payload: MeltRequest, cashu_id: str = Query(None)):
) )
if status.paid == True: if status.paid == True:
await ledger._invalidate_proofs(proofs) await ledger._invalidate_proofs(proofs)
return status.paid, status.preimage return GetMeltResponse(paid=status.paid, preimage=status.preimage)
return False, ""
@cashu_ext.post("/melt")
async def melt(payload: MeltRequest) -> GetMeltResponse:
"""
Requests tokens to be destroyed and sent out via Lightning.
"""
ok, preimage = await ledger.melt(payload.proofs, payload.invoice)
resp = GetMeltResponse(paid=ok, preimage=preimage)
return resp
@cashu_ext.post("/check") @cashu_ext.post("/check")
@ -298,29 +271,46 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
Used by wallets for figuring out the fees they need to supply. Used by wallets for figuring out the fees they need to supply.
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
""" """
fees_msat = await ledger.check_fees(payload.pr) invoice_obj = bolt11.decode(payload.pr)
internal_checking_id = await check_internal(invoice_obj.payment_hash)
if not internal_checking_id:
fees_msat = fee_reserve(invoice_obj.amount_msat)
else:
fees_msat = 0
return CheckFeesResponse(fee=fees_msat / 1000) return CheckFeesResponse(fee=fees_msat / 1000)
@cashu_ext.post("/split") @cashu_ext.post("/{cashu_id}/split")
async def split( async def split(
payload: SplitRequest, payload: SplitRequest, cashu_id: str = Query(None)
) -> Union[CashuError, PostSplitResponse]: ) -> PostSplitResponse:
""" """
Requetst a set of tokens with amount "total" to be split into two Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split". newly minted sets with amount "split" and "total-split".
""" """
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
proofs = payload.proofs proofs = payload.proofs
amount = payload.amount amount = payload.amount
outputs = payload.outputs.blinded_messages if payload.outputs else None outputs = payload.outputs.blinded_messages
# backwards compatibility with clients < v0.2.2 # backwards compatibility with clients < v0.2.2
assert outputs, Exception("no outputs provided.") assert outputs, Exception("no outputs provided.")
try: try:
split_return = await ledger.split(proofs, amount, outputs) split_return = await ledger.split(proofs, amount, outputs, cashu.keyset_id)
except Exception as exc: except Exception as exc:
return CashuError(error=str(exc)) HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=str(exc),
)
if not split_return: if not split_return:
return CashuError(error="there was an error with the split") raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="there was an error with the split",
)
frst_promises, scnd_promises = split_return frst_promises, scnd_promises = split_return
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises) resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
return resp return resp