cleanup
This commit is contained in:
parent
5b1ee554df
commit
53db275fe0
|
@ -11,13 +11,16 @@ db = Database("ext_cashu")
|
|||
|
||||
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")
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
# from .crud import LedgerCrud
|
||||
|
||||
# db = Database("ext_cashu", LNBITS_DATA_FOLDER)
|
||||
|
||||
ledger = Ledger(
|
||||
db=db,
|
||||
# seed=MINT_PRIVATE_KEY,
|
||||
|
@ -26,17 +29,6 @@ ledger = Ledger(
|
|||
)
|
||||
|
||||
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():
|
||||
|
|
|
@ -19,46 +19,6 @@ from cashu.core.base import MintKeyset
|
|||
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(
|
||||
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
|
||||
) -> Cashu:
|
||||
|
@ -85,23 +45,23 @@ async def create_cashu(
|
|||
return cashu
|
||||
|
||||
|
||||
async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]:
|
||||
entropy = bytes([random.getrandbits(8) for i in range(16)])
|
||||
mnemonic = bip39.mnemonic_from_bytes(entropy)
|
||||
seed = bip39.mnemonic_to_seed(mnemonic)
|
||||
root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
|
||||
# async def update_cashu_keys(cashu_id, wif: str = None) -> Optional[Cashu]:
|
||||
# entropy = bytes([random.getrandbits(8) for i in range(16)])
|
||||
# mnemonic = bip39.mnemonic_from_bytes(entropy)
|
||||
# seed = bip39.mnemonic_to_seed(mnemonic)
|
||||
# root = bip32.HDKey.from_seed(seed, version=NETWORKS["main"]["xprv"])
|
||||
|
||||
bip44_xprv = root.derive("m/44h/1h/0h")
|
||||
bip44_xpub = bip44_xprv.to_public()
|
||||
# bip44_xprv = root.derive("m/44h/1h/0h")
|
||||
# bip44_xpub = bip44_xprv.to_public()
|
||||
|
||||
await db.execute(
|
||||
"UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?",
|
||||
bip44_xprv.to_base58(),
|
||||
bip44_xpub.to_base58(),
|
||||
cashu_id,
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||
return Cashu(**row) if row else None
|
||||
# await db.execute(
|
||||
# "UPDATE cashu.cashu SET prv = ?, pub = ? WHERE id = ?",
|
||||
# bip44_xprv.to_base58(),
|
||||
# bip44_xpub.to_base58(),
|
||||
# cashu_id,
|
||||
# )
|
||||
# row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||
# return Cashu(**row) if row else None
|
||||
|
||||
|
||||
async def get_cashu(cashu_id) -> Optional[Cashu]:
|
||||
|
@ -130,103 +90,103 @@ 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
|
||||
):
|
||||
for amount, B_, C_ in zip(amounts, B_s, C_s):
|
||||
await store_promise(amount, B_, C_, cashu_id)
|
||||
# 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)
|
||||
|
||||
|
||||
async def store_promise(amount: int, B_: str, C_: str, cashu_id: str):
|
||||
promise_id = urlsafe_short_hash()
|
||||
# async def store_promise(amount: int, B_: str, C_: str, cashu_id: str):
|
||||
# promise_id = urlsafe_short_hash()
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO cashu.promises
|
||||
(id, amount, B_b, C_b, cashu_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(promise_id, amount, str(B_), str(C_), cashu_id),
|
||||
)
|
||||
# await db.execute(
|
||||
# """
|
||||
# INSERT INTO cashu.promises
|
||||
# (id, amount, B_b, C_b, cashu_id)
|
||||
# VALUES (?, ?, ?, ?, ?)
|
||||
# """,
|
||||
# (promise_id, amount, str(B_), str(C_), cashu_id),
|
||||
# )
|
||||
|
||||
|
||||
async def get_promises(cashu_id) -> Optional[Cashu]:
|
||||
row = await db.fetchall(
|
||||
"SELECT * FROM cashu.promises WHERE cashu_id = ?", (cashu_id,)
|
||||
)
|
||||
return Promises(**row) if row else None
|
||||
# async def get_promises(cashu_id) -> Optional[Cashu]:
|
||||
# row = await db.fetchall(
|
||||
# "SELECT * FROM cashu.promises WHERE cashu_id = ?", (cashu_id,)
|
||||
# )
|
||||
# return Promises(**row) if row else None
|
||||
|
||||
|
||||
async def get_proofs_used(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
# async def get_proofs_used(
|
||||
# db: Database,
|
||||
# conn: Optional[Connection] = None,
|
||||
# ):
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT secret from cashu.proofs_used
|
||||
"""
|
||||
)
|
||||
return [row[0] for row in rows]
|
||||
# rows = await (conn or db).fetchall(
|
||||
# """
|
||||
# SELECT secret from cashu.proofs_used
|
||||
# """
|
||||
# )
|
||||
# return [row[0] for row in rows]
|
||||
|
||||
|
||||
async def invalidate_proof(cashu_id: str, proof: Proof):
|
||||
invalidate_proof_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO cashu.proofs_used
|
||||
(id, amount, C, secret, cashu_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(invalidate_proof_id, proof.amount, str(proof.C), str(proof.secret), cashu_id),
|
||||
)
|
||||
# async def invalidate_proof(cashu_id: str, proof: Proof):
|
||||
# invalidate_proof_id = urlsafe_short_hash()
|
||||
# await db.execute(
|
||||
# """
|
||||
# INSERT INTO cashu.proofs_used
|
||||
# (id, amount, C, secret, cashu_id)
|
||||
# VALUES (?, ?, ?, ?, ?)
|
||||
# """,
|
||||
# (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):
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO cashu.invoices
|
||||
(cashu_id, amount, pr, hash, issued)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
cashu_id,
|
||||
invoice.amount,
|
||||
invoice.pr,
|
||||
invoice.hash,
|
||||
invoice.issued,
|
||||
),
|
||||
)
|
||||
# async def store_lightning_invoice(cashu_id: str, invoice: Invoice):
|
||||
# await db.execute(
|
||||
# """
|
||||
# INSERT INTO cashu.invoices
|
||||
# (cashu_id, amount, pr, hash, issued)
|
||||
# VALUES (?, ?, ?, ?, ?)
|
||||
# """,
|
||||
# (
|
||||
# cashu_id,
|
||||
# invoice.amount,
|
||||
# invoice.pr,
|
||||
# invoice.hash,
|
||||
# invoice.issued,
|
||||
# ),
|
||||
# )
|
||||
|
||||
|
||||
async def get_lightning_invoice(cashu_id: str, hash: str):
|
||||
row = await db.fetchone(
|
||||
"""
|
||||
SELECT * from cashu.invoices
|
||||
WHERE cashu_id =? AND hash = ?
|
||||
""",
|
||||
(
|
||||
cashu_id,
|
||||
hash,
|
||||
),
|
||||
)
|
||||
return Invoice.from_row(row)
|
||||
# async def get_lightning_invoice(cashu_id: str, hash: str):
|
||||
# row = await db.fetchone(
|
||||
# """
|
||||
# SELECT * from cashu.invoices
|
||||
# WHERE cashu_id =? AND hash = ?
|
||||
# """,
|
||||
# (
|
||||
# cashu_id,
|
||||
# hash,
|
||||
# ),
|
||||
# )
|
||||
# return Invoice.from_row(row)
|
||||
|
||||
|
||||
async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool):
|
||||
await db.execute(
|
||||
"UPDATE cashu.invoices SET issued = ? WHERE cashu_id = ? AND hash = ?",
|
||||
(
|
||||
issued,
|
||||
cashu_id,
|
||||
hash,
|
||||
),
|
||||
)
|
||||
# async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool):
|
||||
# await db.execute(
|
||||
# "UPDATE cashu.invoices SET issued = ? WHERE cashu_id = ? AND hash = ?",
|
||||
# (
|
||||
# issued,
|
||||
# cashu_id,
|
||||
# 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 json
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.tasks import internal_invoice_queue, register_invoice_listener
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import get_cashu
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append("/Users/cc/git/cashu")
|
||||
from cashu.mint import migrations
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from . import db, ledger
|
||||
|
@ -35,51 +29,6 @@ async def wait_for_paid_invoices():
|
|||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") == "cashu" and payment.extra.get("tipSplitted"):
|
||||
# already splitted, ignore
|
||||
if not payment.extra.get("tag") == "cashu":
|
||||
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
|
||||
|
|
|
@ -2,6 +2,7 @@ import json
|
|||
from http import HTTPStatus
|
||||
from typing import Union
|
||||
import math
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import httpx
|
||||
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.helpers import urlsafe_short_hash
|
||||
from lnbits.core.crud import check_internal
|
||||
|
||||
# --------- extension imports
|
||||
|
||||
from . import cashu_ext
|
||||
from .core.base import CashuError, PostSplitResponse, SplitRequest
|
||||
from .crud import (
|
||||
create_cashu,
|
||||
delete_cashu,
|
||||
get_cashu,
|
||||
get_cashus,
|
||||
get_lightning_invoice,
|
||||
store_lightning_invoice,
|
||||
store_promise,
|
||||
update_lightning_invoice,
|
||||
)
|
||||
|
||||
# from .ledger import mint, request_mint
|
||||
from .mint import generate_promises, get_pubkeys, melt, split
|
||||
from .models import (
|
||||
Cashu,
|
||||
CheckPayload,
|
||||
Invoice,
|
||||
MeltPayload,
|
||||
MintPayloads,
|
||||
PayLnurlWData,
|
||||
Pegs,
|
||||
SplitPayload,
|
||||
)
|
||||
from .models import Cashu
|
||||
|
||||
from . import ledger
|
||||
|
||||
############### IMPORT CALLE
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from secp256k1 import PublicKey
|
||||
|
||||
# -------- cashu imports
|
||||
from cashu.core.base import (
|
||||
Proof,
|
||||
BlindedSignature,
|
||||
|
@ -69,9 +54,8 @@ from cashu.core.base import (
|
|||
MintRequest,
|
||||
PostSplitResponse,
|
||||
SplitRequest,
|
||||
Invoice,
|
||||
)
|
||||
from cashu.core.errors import CashuError
|
||||
from . import db, ledger
|
||||
|
||||
LIGHTNING = False
|
||||
|
||||
|
@ -165,7 +149,7 @@ async def mint_coins(
|
|||
data: MintRequest,
|
||||
cashu_id: str = Query(None),
|
||||
payment_hash: str = Query(None),
|
||||
):
|
||||
) -> List[BlindedSignature]:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
Call this endpoint after `GET /mint`.
|
||||
|
@ -220,7 +204,9 @@ async def mint_coins(
|
|||
|
||||
|
||||
@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."""
|
||||
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
|
@ -229,8 +215,6 @@ async def melt_coins(payload: MeltRequest, cashu_id: str = Query(None)):
|
|||
)
|
||||
proofs = payload.proofs
|
||||
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
|
||||
# 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:
|
||||
await ledger._invalidate_proofs(proofs)
|
||||
return status.paid, 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
|
||||
return GetMeltResponse(paid=status.paid, preimage=status.preimage)
|
||||
|
||||
|
||||
@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.
|
||||
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)
|
||||
|
||||
|
||||
@cashu_ext.post("/split")
|
||||
@cashu_ext.post("/{cashu_id}/split")
|
||||
async def split(
|
||||
payload: SplitRequest,
|
||||
) -> Union[CashuError, PostSplitResponse]:
|
||||
payload: SplitRequest, cashu_id: str = Query(None)
|
||||
) -> PostSplitResponse:
|
||||
"""
|
||||
Requetst a set of tokens with amount "total" to be split into two
|
||||
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
|
||||
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
|
||||
assert outputs, Exception("no outputs provided.")
|
||||
try:
|
||||
split_return = await ledger.split(proofs, amount, outputs)
|
||||
split_return = await ledger.split(proofs, amount, outputs, cashu.keyset_id)
|
||||
except Exception as exc:
|
||||
return CashuError(error=str(exc))
|
||||
HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
)
|
||||
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
|
||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||
return resp
|
||||
|
|
Loading…
Reference in New Issue
Block a user