cleanup
This commit is contained in:
parent
5b1ee554df
commit
53db275fe0
|
@ -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():
|
||||||
|
|
|
@ -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,
|
||||||
),
|
# ),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user