mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-04 01:14:21 +01:00
Merge pull request #36 from callebtc/lnbits_importable_2
Importable lib
This commit is contained in:
@@ -24,9 +24,9 @@ class P2SHScript(BaseModel):
|
||||
|
||||
class Proof(BaseModel):
|
||||
id: str = ""
|
||||
amount: int
|
||||
amount: int = 0
|
||||
secret: str = ""
|
||||
C: str
|
||||
C: str = ""
|
||||
script: Union[P2SHScript, None] = None
|
||||
reserved: bool = False # whether this proof is reserved for sending
|
||||
send_id: str = "" # unique ID of send attempt
|
||||
@@ -95,7 +95,6 @@ class Invoice(BaseModel):
|
||||
|
||||
|
||||
class BlindedMessage(BaseModel):
|
||||
id: str = ""
|
||||
amount: int
|
||||
B_: str
|
||||
|
||||
@@ -262,6 +261,7 @@ class MintKeyset:
|
||||
self.generate_keys(seed)
|
||||
|
||||
def generate_keys(self, seed):
|
||||
"""Generates keys of a keyset from a seed."""
|
||||
self.private_keys = derive_keys(seed, self.derivation_path)
|
||||
self.public_keys = derive_pubkeys(self.private_keys)
|
||||
self.id = derive_keyset_id(self.public_keys)
|
||||
|
||||
@@ -5,6 +5,14 @@ from typing import Dict, List
|
||||
from cashu.core.secp import PrivateKey, PublicKey
|
||||
from cashu.core.settings import MAX_ORDER
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
def derive_keys(master_key: str, derivation_path: str = ""):
|
||||
"""
|
||||
@@ -29,7 +37,9 @@ def derive_pubkeys(keys: Dict[int, PrivateKey]):
|
||||
|
||||
def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in keys.items()])
|
||||
# sort public keys by amount
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()])
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
|
||||
@@ -3,13 +3,17 @@ import re
|
||||
from cashu.core.db import COCKROACH, POSTGRES, SQLITE, Database
|
||||
|
||||
|
||||
def table_with_schema(db, table: str):
|
||||
return f"{db.references_schema if db.schema else ''}{table}"
|
||||
|
||||
|
||||
async def migrate_databases(db: Database, migrations_module):
|
||||
"""Creates the necessary databases if they don't exist already; or migrates them."""
|
||||
|
||||
async def set_migration_version(conn, db_name, version):
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO dbversions (db, version) VALUES (?, ?)
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'dbversions')} (db, version) VALUES (?, ?)
|
||||
ON CONFLICT (db) DO UPDATE SET version = ?
|
||||
""",
|
||||
(db_name, version, version),
|
||||
@@ -18,7 +22,7 @@ async def migrate_databases(db: Database, migrations_module):
|
||||
async def run_migration(db, migrations_module):
|
||||
db_name = migrations_module.__name__.split(".")[-2]
|
||||
for key, migrate in migrations_module.__dict__.items():
|
||||
match = match = matcher.match(key)
|
||||
match = matcher.match(key)
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
@@ -33,17 +37,19 @@ async def migrate_databases(db: Database, migrations_module):
|
||||
async with db.connect() as conn:
|
||||
if conn.type == SQLITE:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
|
||||
f"SELECT * FROM sqlite_master WHERE type='table' AND name='{table_with_schema(db, 'dbversions')}'"
|
||||
)
|
||||
elif conn.type in {POSTGRES, COCKROACH}:
|
||||
exists = await conn.fetchone(
|
||||
"SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'"
|
||||
f"SELECT * FROM information_schema.tables WHERE table_name = '{table_with_schema(db, 'dbversions')}'"
|
||||
)
|
||||
|
||||
if not exists:
|
||||
await migrations_module.m000_create_migrations_table(conn)
|
||||
|
||||
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
|
||||
rows = await (
|
||||
await conn.execute(f"SELECT * FROM {table_with_schema(db, 'dbversions')}")
|
||||
).fetchall()
|
||||
current_versions = {row["db"]: row["version"] for row in rows}
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
await run_migration(conn, migrations_module)
|
||||
await run_migration(db, migrations_module)
|
||||
|
||||
@@ -48,4 +48,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.3.3"
|
||||
VERSION = "0.4.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from cashu.lightning.lnbits import LNbitsWallet
|
||||
# from cashu.lightning.lnbits import LNbitsWallet
|
||||
|
||||
WALLET = LNbitsWallet()
|
||||
# WALLET = LNbitsWallet()
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
from cashu.core.settings import MINT_PRIVATE_KEY
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
ledger = Ledger(MINT_PRIVATE_KEY, "data/mint", derivation_path="0/0/0/0")
|
||||
|
||||
@@ -5,24 +5,25 @@ from fastapi import FastAPI
|
||||
from loguru import logger
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette_context import context
|
||||
from starlette_context.middleware import RawContextMiddleware
|
||||
|
||||
from cashu.core.settings import DEBUG, VERSION
|
||||
|
||||
from .router import router
|
||||
from .startup import load_ledger
|
||||
from .startup import start_mint_init
|
||||
|
||||
# from starlette_context import context
|
||||
# from starlette_context.middleware import RawContextMiddleware
|
||||
|
||||
|
||||
class CustomHeaderMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware for starlette that can set the context from request headers
|
||||
"""
|
||||
# class CustomHeaderMiddleware(BaseHTTPMiddleware):
|
||||
# """
|
||||
# Middleware for starlette that can set the context from request headers
|
||||
# """
|
||||
|
||||
async def dispatch(self, request, call_next):
|
||||
context["client-version"] = request.headers.get("Client-version")
|
||||
response = await call_next(request)
|
||||
return response
|
||||
# async def dispatch(self, request, call_next):
|
||||
# context["client-version"] = request.headers.get("Client-version")
|
||||
# response = await call_next(request)
|
||||
# return response
|
||||
|
||||
|
||||
def create_app(config_object="core.settings") -> FastAPI:
|
||||
@@ -60,12 +61,12 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
|
||||
configure_logger()
|
||||
|
||||
middleware = [
|
||||
Middleware(
|
||||
RawContextMiddleware,
|
||||
),
|
||||
Middleware(CustomHeaderMiddleware),
|
||||
]
|
||||
# middleware = [
|
||||
# Middleware(
|
||||
# RawContextMiddleware,
|
||||
# ),
|
||||
# Middleware(CustomHeaderMiddleware),
|
||||
# ]
|
||||
|
||||
app = FastAPI(
|
||||
title="Cashu Mint",
|
||||
@@ -75,7 +76,7 @@ def create_app(config_object="core.settings") -> FastAPI:
|
||||
"name": "MIT License",
|
||||
"url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE",
|
||||
},
|
||||
middleware=middleware,
|
||||
# middleware=middleware,
|
||||
)
|
||||
return app
|
||||
|
||||
@@ -86,5 +87,5 @@ app.include_router(router=router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_load_ledger():
|
||||
await load_ledger()
|
||||
async def startup_mint():
|
||||
await start_mint_init()
|
||||
|
||||
@@ -2,19 +2,60 @@ from typing import Any, List, Optional
|
||||
|
||||
from cashu.core.base import Invoice, MintKeyset, Proof
|
||||
from cashu.core.db import Connection, Database
|
||||
from cashu.core.migrations import table_with_schema
|
||||
|
||||
|
||||
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 store_promise(
|
||||
db: Database,
|
||||
amount: int,
|
||||
B_: str,
|
||||
C_: str,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO promises
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'promises')}
|
||||
(amount, B_b, C_b)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
@@ -32,23 +73,23 @@ async def get_proofs_used(
|
||||
):
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
"""
|
||||
SELECT secret from proofs_used
|
||||
f"""
|
||||
SELECT secret from {table_with_schema(db, 'proofs_used')}
|
||||
"""
|
||||
)
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
||||
async def invalidate_proof(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO proofs_used
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_used')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
@@ -61,14 +102,14 @@ async def invalidate_proof(
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
invoice: Invoice,
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
INSERT INTO invoices
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'invoices')}
|
||||
(amount, pr, hash, issued)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
@@ -82,14 +123,14 @@ async def store_lightning_invoice(
|
||||
|
||||
|
||||
async def get_lightning_invoice(
|
||||
hash: str,
|
||||
db: Database,
|
||||
hash: str,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT * from invoices
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'invoices')}
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(hash,),
|
||||
@@ -98,13 +139,13 @@ async def get_lightning_invoice(
|
||||
|
||||
|
||||
async def update_lightning_invoice(
|
||||
db: Database,
|
||||
hash: str,
|
||||
issued: bool,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
await (conn or db).execute(
|
||||
"UPDATE invoices SET issued = ? WHERE hash = ?",
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET issued = ? WHERE hash = ?",
|
||||
(
|
||||
issued,
|
||||
hash,
|
||||
@@ -113,23 +154,23 @@ async def update_lightning_invoice(
|
||||
|
||||
|
||||
async def store_keyset(
|
||||
db: Database,
|
||||
keyset: MintKeyset,
|
||||
db: Database = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute( # type: ignore
|
||||
"""
|
||||
INSERT INTO keysets
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'keysets')}
|
||||
(id, derivation_path, valid_from, valid_to, first_seen, active, version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
keyset.id,
|
||||
keyset.derivation_path,
|
||||
keyset.valid_from,
|
||||
keyset.valid_to,
|
||||
keyset.first_seen,
|
||||
keyset.valid_from or db.timestamp_now,
|
||||
keyset.valid_to or db.timestamp_now,
|
||||
keyset.first_seen or db.timestamp_now,
|
||||
True,
|
||||
keyset.version,
|
||||
),
|
||||
@@ -137,9 +178,9 @@ async def store_keyset(
|
||||
|
||||
|
||||
async def get_keyset(
|
||||
db: Database,
|
||||
id: str = None,
|
||||
derivation_path: str = "",
|
||||
db: Database = None,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
clauses = []
|
||||
@@ -158,7 +199,7 @@ async def get_keyset(
|
||||
|
||||
rows = await (conn or db).fetchall( # type: ignore
|
||||
f"""
|
||||
SELECT * from keysets
|
||||
SELECT * from {table_with_schema(db, 'keysets')}
|
||||
{where}
|
||||
""",
|
||||
tuple(values),
|
||||
|
||||
@@ -6,7 +6,6 @@ import math
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from loguru import logger
|
||||
from starlette_context import context
|
||||
|
||||
import cashu.core.b_dhke as b_dhke
|
||||
import cashu.core.bolt11 as bolt11
|
||||
@@ -19,79 +18,88 @@ from cashu.core.base import (
|
||||
MintKeysets,
|
||||
Proof,
|
||||
)
|
||||
from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.helpers import fee_reserve, sum_proofs
|
||||
from cashu.core.script import verify_script
|
||||
from cashu.core.secp import PublicKey
|
||||
from cashu.core.settings import LIGHTNING, MAX_ORDER, VERSION
|
||||
from cashu.core.split import amount_split
|
||||
from cashu.lightning import WALLET
|
||||
from cashu.mint.crud import (
|
||||
get_keyset,
|
||||
get_lightning_invoice,
|
||||
get_proofs_used,
|
||||
invalidate_proof,
|
||||
store_keyset,
|
||||
store_lightning_invoice,
|
||||
store_promise,
|
||||
update_lightning_invoice,
|
||||
)
|
||||
from cashu.mint.crud import LedgerCrud
|
||||
|
||||
# from starlette_context import context
|
||||
|
||||
|
||||
class Ledger:
|
||||
def __init__(self, secret_key: str, db: str, derivation_path=""):
|
||||
def __init__(
|
||||
self,
|
||||
db: Database,
|
||||
seed: str,
|
||||
derivation_path="",
|
||||
crud=LedgerCrud,
|
||||
lightning=None,
|
||||
):
|
||||
self.proofs_used: Set[str] = set()
|
||||
self.master_key = secret_key
|
||||
self.master_key = seed
|
||||
self.derivation_path = derivation_path
|
||||
self.db: Database = Database("mint", db)
|
||||
|
||||
self.db = db
|
||||
self.crud = crud
|
||||
self.lightning = lightning
|
||||
|
||||
async def load_used_proofs(self):
|
||||
"""Load all used proofs from database."""
|
||||
self.proofs_used = set(await get_proofs_used(db=self.db))
|
||||
proofs_used = await self.crud.get_proofs_used(db=self.db)
|
||||
self.proofs_used = set(proofs_used)
|
||||
|
||||
async def init_keysets(self):
|
||||
"""Loads all past keysets and stores the active one if not already in db"""
|
||||
# generate current keyset from seed and current derivation path
|
||||
self.keyset = MintKeyset(
|
||||
seed=self.master_key, derivation_path=self.derivation_path, version=VERSION
|
||||
async def load_keyset(self, derivation_path):
|
||||
"""Load current keyset keyset or generate new one."""
|
||||
keyset = MintKeyset(
|
||||
seed=self.master_key, derivation_path=derivation_path, version=VERSION
|
||||
)
|
||||
# check if current keyset is stored in db and store if not
|
||||
logger.debug(f"Loading keyset {self.keyset.id} from db.")
|
||||
current_keyset_local: List[MintKeyset] = await get_keyset(
|
||||
id=self.keyset.id, db=self.db
|
||||
logger.debug(f"Loading keyset {keyset.id} from db.")
|
||||
tmp_keyset_local: List[MintKeyset] = await self.crud.get_keyset(
|
||||
id=keyset.id, db=self.db
|
||||
)
|
||||
if not len(current_keyset_local):
|
||||
logger.debug(f"Storing keyset {self.keyset.id}.")
|
||||
await store_keyset(keyset=self.keyset, db=self.db)
|
||||
if not len(tmp_keyset_local):
|
||||
logger.debug(f"Storing keyset {keyset.id}.")
|
||||
await self.crud.store_keyset(keyset=keyset, db=self.db)
|
||||
return keyset
|
||||
|
||||
async def init_keysets(self):
|
||||
"""Loads all keysets from db."""
|
||||
self.keyset = await self.load_keyset(self.derivation_path)
|
||||
# load all past keysets from db
|
||||
# this needs two steps because the types of tmp_keysets and the argument of MintKeysets() are different
|
||||
tmp_keysets: List[MintKeyset] = await get_keyset(db=self.db)
|
||||
tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db)
|
||||
self.keysets = MintKeysets(tmp_keysets)
|
||||
logger.debug(f"Keysets {self.keysets.keysets}")
|
||||
logger.debug(f"Loading {len(self.keysets.keysets)} keysets form db.")
|
||||
# generate all derived keys from stored derivation paths of past keysets
|
||||
for _, v in self.keysets.keysets.items():
|
||||
logger.debug(f"Generating keys for keyset {v.id}")
|
||||
v.generate_keys(self.master_key)
|
||||
|
||||
if len(self.keysets.keysets):
|
||||
logger.debug(f"Loaded {len(self.keysets.keysets)} keysets from db.")
|
||||
|
||||
async def _generate_promises(self, amounts: List[int], B_s: List[str]):
|
||||
async def _generate_promises(
|
||||
self, B_s: List[BlindedMessage], keyset: MintKeyset = None
|
||||
):
|
||||
"""Generates promises that sum to the given amount."""
|
||||
return [
|
||||
await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True))
|
||||
for (amount, B_) in zip(amounts, B_s)
|
||||
await self._generate_promise(
|
||||
b.amount, PublicKey(bytes.fromhex(b.B_), raw=True), keyset
|
||||
)
|
||||
for b in B_s
|
||||
]
|
||||
|
||||
async def _generate_promise(self, amount: int, B_: PublicKey):
|
||||
async def _generate_promise(
|
||||
self, amount: int, B_: PublicKey, keyset: MintKeyset = None
|
||||
):
|
||||
"""Generates a promise for given amount and returns a pair (amount, C')."""
|
||||
secret_key = self.keyset.private_keys[amount] # Get the correct key
|
||||
C_ = b_dhke.step2_bob(B_, secret_key)
|
||||
await store_promise(
|
||||
amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
||||
keyset = keyset if keyset else self.keyset
|
||||
private_key_amount = keyset.private_keys[amount]
|
||||
C_ = b_dhke.step2_bob(B_, private_key_amount)
|
||||
await self.crud.store_promise(
|
||||
amount=amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
|
||||
)
|
||||
return BlindedSignature(amount=amount, C_=C_.serialize().hex())
|
||||
return BlindedSignature(id=keyset.id, amount=amount, C_=C_.serialize().hex())
|
||||
|
||||
def _check_spendable(self, proof: Proof):
|
||||
"""Checks whether the proof was already spent."""
|
||||
@@ -109,22 +117,24 @@ class Ledger:
|
||||
raise Exception(f"tokens already spent. Secret: {proof.secret}")
|
||||
# if no keyset id is given in proof, assume the current one
|
||||
if not proof.id:
|
||||
secret_key = self.keyset.private_keys[proof.amount]
|
||||
private_key_amount = self.keyset.private_keys[proof.amount]
|
||||
else:
|
||||
# use the appropriate active keyset for this proof.id
|
||||
secret_key = self.keysets.keysets[proof.id].private_keys[proof.amount]
|
||||
private_key_amount = self.keysets.keysets[proof.id].private_keys[
|
||||
proof.amount
|
||||
]
|
||||
|
||||
C = PublicKey(bytes.fromhex(proof.C), raw=True)
|
||||
|
||||
# backwards compatibility with old hash_to_curve < 0.3.3
|
||||
# backwards compatibility with old hash_to_curve < 0.4.0
|
||||
try:
|
||||
ret = legacy.verify_pre_0_3_3(secret_key, C, proof.secret)
|
||||
ret = legacy.verify_pre_0_3_3(private_key_amount, C, proof.secret)
|
||||
if ret:
|
||||
return ret
|
||||
except:
|
||||
pass
|
||||
|
||||
return b_dhke.verify(secret_key, C, proof.secret)
|
||||
return b_dhke.verify(private_key_amount, C, proof.secret)
|
||||
|
||||
def _verify_script(self, idx: int, proof: Proof):
|
||||
"""
|
||||
@@ -209,17 +219,22 @@ class Ledger:
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int):
|
||||
"""Returns an invoice from the Lightning backend."""
|
||||
error, balance = await WALLET.status()
|
||||
error, balance = await self.lightning.status()
|
||||
if error:
|
||||
raise Exception(f"Lightning wallet not responding: {error}")
|
||||
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
|
||||
amount, "cashu deposit"
|
||||
)
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
payment_request,
|
||||
error_message,
|
||||
) = await self.lightning.create_invoice(amount, "cashu deposit")
|
||||
return payment_request, checking_id
|
||||
|
||||
async def _check_lightning_invoice(self, amounts, payment_hash: str):
|
||||
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
|
||||
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
|
||||
invoice: Invoice = await self.crud.get_lightning_invoice(
|
||||
hash=payment_hash, db=self.db
|
||||
)
|
||||
if invoice.issued:
|
||||
raise Exception("tokens already issued for this invoice.")
|
||||
total_requested = sum(amounts)
|
||||
@@ -227,19 +242,25 @@ class Ledger:
|
||||
raise Exception(
|
||||
f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
status = await WALLET.get_invoice_status(payment_hash)
|
||||
status = await self.lightning.get_invoice_status(payment_hash)
|
||||
if status.paid:
|
||||
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=payment_hash, issued=True, db=self.db
|
||||
)
|
||||
return status.paid
|
||||
|
||||
async def _pay_lightning_invoice(self, invoice: str, fees_msat: int):
|
||||
"""Returns an invoice from the Lightning backend."""
|
||||
error, _ = await WALLET.status()
|
||||
error, _ = await self.lightning.status()
|
||||
if error:
|
||||
raise Exception(f"Lightning wallet not responding: {error}")
|
||||
ok, checking_id, fee_msat, preimage, error_message = await WALLET.pay_invoice(
|
||||
invoice, fee_limit_msat=fees_msat
|
||||
)
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
fee_msat,
|
||||
preimage,
|
||||
error_message,
|
||||
) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fees_msat)
|
||||
return ok, preimage
|
||||
|
||||
async def _invalidate_proofs(self, proofs: List[Proof]):
|
||||
@@ -249,15 +270,12 @@ class Ledger:
|
||||
self.proofs_used |= proof_msgs
|
||||
# store in db
|
||||
for p in proofs:
|
||||
await invalidate_proof(p, db=self.db)
|
||||
|
||||
def _serialize_pubkeys(self):
|
||||
"""Returns public keys for possible amounts."""
|
||||
return {a: p.serialize().hex() for a, p in self.keyset.public_keys.items()}
|
||||
await self.crud.invalidate_proof(proof=p, db=self.db)
|
||||
|
||||
# Public methods
|
||||
def get_keyset(self):
|
||||
return self._serialize_pubkeys()
|
||||
def get_keyset(self, keyset_id: str = None):
|
||||
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
|
||||
return {a: p.serialize().hex() for a, p in keyset.public_keys.items()}
|
||||
|
||||
async def request_mint(self, amount):
|
||||
"""Returns Lightning invoice and stores it in the db."""
|
||||
@@ -267,11 +285,17 @@ class Ledger:
|
||||
)
|
||||
if not payment_request or not checking_id:
|
||||
raise Exception(f"Could not create Lightning invoice.")
|
||||
await store_lightning_invoice(invoice, db=self.db)
|
||||
await self.crud.store_lightning_invoice(invoice=invoice, db=self.db)
|
||||
return payment_request, checking_id
|
||||
|
||||
async def mint(self, B_s: List[PublicKey], amounts: List[int], payment_hash=None):
|
||||
async def mint(
|
||||
self,
|
||||
B_s: List[BlindedMessage],
|
||||
payment_hash=None,
|
||||
keyset: MintKeyset = None,
|
||||
):
|
||||
"""Mints a promise for coins for B_."""
|
||||
amounts = [b.amount for b in B_s]
|
||||
# check if lightning invoice was paid
|
||||
if LIGHTNING:
|
||||
try:
|
||||
@@ -285,9 +309,7 @@ class Ledger:
|
||||
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 self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts)
|
||||
]
|
||||
promises = await self._generate_promises(B_s, keyset)
|
||||
return promises
|
||||
|
||||
async def melt(self, proofs: List[Proof], invoice: str):
|
||||
@@ -304,7 +326,10 @@ class Ledger:
|
||||
"provided proofs not enough for Lightning payment."
|
||||
)
|
||||
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
if LIGHTNING:
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
else:
|
||||
status, preimage = True, "preimage"
|
||||
if status == True:
|
||||
await self._invalidate_proofs(proofs)
|
||||
return status, preimage
|
||||
@@ -319,13 +344,20 @@ class Ledger:
|
||||
amount = math.ceil(decoded_invoice.amount_msat / 1000)
|
||||
# hack: check if it's internal, if it exists, it will return paid = False,
|
||||
# if id does not exist (not internal), it returns paid = None
|
||||
paid = await WALLET.get_invoice_status(decoded_invoice.payment_hash)
|
||||
internal = paid.paid == False
|
||||
if LIGHTNING:
|
||||
paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash)
|
||||
internal = paid.paid == False
|
||||
else:
|
||||
internal = True
|
||||
fees_msat = fee_reserve(amount * 1000, internal)
|
||||
return fees_msat
|
||||
|
||||
async def split(
|
||||
self, proofs: List[Proof], amount: int, outputs: List[BlindedMessage]
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
amount: int,
|
||||
outputs: List[BlindedMessage],
|
||||
keyset: MintKeyset = None,
|
||||
):
|
||||
"""Consumes proofs and prepares new promises based on the amount split."""
|
||||
total = sum_proofs(proofs)
|
||||
@@ -355,13 +387,16 @@ class Ledger:
|
||||
# Mark proofs as used and prepare new promises
|
||||
await self._invalidate_proofs(proofs)
|
||||
|
||||
# split outputs according to amount
|
||||
outs_fst = amount_split(total - amount)
|
||||
outs_snd = amount_split(amount)
|
||||
B_fst = [od.B_ for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od.B_ for od in outputs[len(outs_fst) :]]
|
||||
B_fst = [od for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od for od in outputs[len(outs_fst) :]]
|
||||
|
||||
# generate promises
|
||||
prom_fst, prom_snd = await self._generate_promises(
|
||||
outs_fst, B_fst
|
||||
), await self._generate_promises(outs_snd, B_snd)
|
||||
B_fst, keyset
|
||||
), await self._generate_promises(B_snd, keyset)
|
||||
|
||||
# verify amounts in produced proofs
|
||||
self._verify_equation_balanced(proofs, prom_fst + prom_snd)
|
||||
return prom_fst, prom_snd
|
||||
|
||||
@@ -22,7 +22,8 @@ def main(
|
||||
ssl_keyfile: str = None,
|
||||
ssl_certfile: str = None,
|
||||
):
|
||||
"""Launched with `poetry run mint` at root level"""
|
||||
"""This routine starts the uvicorn server if the Cashu mint is
|
||||
launched with `poetry run mint` at root level"""
|
||||
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
|
||||
d = dict()
|
||||
for a in ctx.args:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.migrations import table_with_schema
|
||||
|
||||
|
||||
async def m000_create_migrations_table(db):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dbversions (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'dbversions')} (
|
||||
db TEXT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
)
|
||||
@@ -14,8 +15,8 @@ async def m000_create_migrations_table(db):
|
||||
|
||||
async def m001_initial(db: Database):
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS promises (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'promises')} (
|
||||
amount INTEGER NOT NULL,
|
||||
B_b TEXT NOT NULL,
|
||||
C_b TEXT NOT NULL,
|
||||
@@ -27,8 +28,8 @@ async def m001_initial(db: Database):
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS proofs_used (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_used')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
@@ -40,8 +41,8 @@ async def m001_initial(db: Database):
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'invoices')} (
|
||||
amount INTEGER NOT NULL,
|
||||
pr TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
@@ -53,38 +54,38 @@ async def m001_initial(db: Database):
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance_issued AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount) AS s
|
||||
FROM promises
|
||||
WHERE amount > 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance_issued')} AS
|
||||
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
# SELECT SUM(amount) AS s
|
||||
# FROM {table_with_schema(db, 'promises')}
|
||||
# WHERE amount > 0
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance_used AS
|
||||
SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
SELECT SUM(amount) AS s
|
||||
FROM proofs_used
|
||||
WHERE amount > 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance_used')} AS
|
||||
# SELECT COALESCE(SUM(s), 0) AS balance FROM (
|
||||
# SELECT SUM(amount) AS s
|
||||
# FROM {table_with_schema(db, 'proofs_used')}
|
||||
# WHERE amount > 0
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE VIEW IF NOT EXISTS balance AS
|
||||
SELECT s_issued - s_used AS balance FROM (
|
||||
SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
FROM balance_issued bi
|
||||
CROSS JOIN balance_used bu
|
||||
);
|
||||
"""
|
||||
)
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE VIEW {table_with_schema(db, 'balance')} AS
|
||||
# SELECT s_issued - s_used AS balance FROM (
|
||||
# SELECT bi.balance AS s_issued, bu.balance AS s_used
|
||||
# FROM {table_with_schema(db, 'balance_issued')} bi
|
||||
# CROSS JOIN {table_with_schema(db, 'balance_used')} bu
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
|
||||
|
||||
async def m003_mint_keysets(db: Database):
|
||||
@@ -93,12 +94,12 @@ async def m003_mint_keysets(db: Database):
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS keysets (
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'keysets')} (
|
||||
id TEXT NOT NULL,
|
||||
derivation_path TEXT,
|
||||
valid_from TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
valid_to TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
first_seen TIMESTAMP DEFAULT {db.timestamp_now},
|
||||
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)
|
||||
@@ -108,7 +109,7 @@ async def m003_mint_keysets(db: Database):
|
||||
)
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS mint_pubkeys (
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'mint_pubkeys')} (
|
||||
id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
@@ -124,4 +125,6 @@ async def m004_keysets_add_version(db: Database):
|
||||
"""
|
||||
Column that remembers with which version
|
||||
"""
|
||||
await db.execute("ALTER TABLE keysets ADD COLUMN version TEXT")
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||
)
|
||||
|
||||
@@ -16,19 +16,20 @@ from cashu.core.base import (
|
||||
SplitRequest,
|
||||
)
|
||||
from cashu.core.errors import CashuError
|
||||
from cashu.mint import ledger
|
||||
from cashu.mint.startup import ledger
|
||||
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
|
||||
@router.get("/keys")
|
||||
def keys() -> dict[int, str]:
|
||||
async def keys() -> dict[int, str]:
|
||||
"""Get the public keys of the mint"""
|
||||
return ledger.get_keyset()
|
||||
keyset = ledger.get_keyset()
|
||||
return keyset
|
||||
|
||||
|
||||
@router.get("/keysets")
|
||||
def keysets() -> dict[str, list[str]]:
|
||||
async def keysets() -> dict[str, list[str]]:
|
||||
"""Get all active keysets of the mint"""
|
||||
return {"keysets": ledger.keysets.get_ids()}
|
||||
|
||||
@@ -49,7 +50,7 @@ async def request_mint(amount: int = 0) -> GetMintResponse:
|
||||
|
||||
@router.post("/mint")
|
||||
async def mint(
|
||||
payloads: MintRequest,
|
||||
mint_request: MintRequest,
|
||||
payment_hash: Union[str, None] = None,
|
||||
) -> Union[List[BlindedSignature], CashuError]:
|
||||
"""
|
||||
@@ -57,13 +58,10 @@ async def mint(
|
||||
|
||||
Call this endpoint after `GET /mint`.
|
||||
"""
|
||||
amounts = []
|
||||
B_s = []
|
||||
for payload in payloads.blinded_messages:
|
||||
amounts.append(payload.amount)
|
||||
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
|
||||
try:
|
||||
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
|
||||
promises = await ledger.mint(
|
||||
mint_request.blinded_messages, payment_hash=payment_hash
|
||||
)
|
||||
return promises
|
||||
except Exception as exc:
|
||||
return CashuError(error=str(exc))
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
# startup routine of the standalone app. These are the steps that need
|
||||
# to be taken by external apps importing the cashu mint.
|
||||
|
||||
import asyncio
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import CASHU_DIR, LIGHTNING
|
||||
from cashu.lightning import WALLET
|
||||
from cashu.core.settings import CASHU_DIR, LIGHTNING, MINT_PRIVATE_KEY
|
||||
from cashu.lightning.lnbits import LNbitsWallet
|
||||
from cashu.mint import migrations
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
from . import ledger
|
||||
ledger = Ledger(
|
||||
db=Database("mint", "data/mint"),
|
||||
seed=MINT_PRIVATE_KEY,
|
||||
# seed="asd",
|
||||
derivation_path="0/0/0/0",
|
||||
lightning=LNbitsWallet() if LIGHTNING else None,
|
||||
)
|
||||
|
||||
|
||||
async def load_ledger():
|
||||
async def start_mint_init():
|
||||
|
||||
await migrate_databases(ledger.db, migrations)
|
||||
# await asyncio.wait([m001_initial(ledger.db)])
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
if LIGHTNING:
|
||||
error_message, balance = await WALLET.status()
|
||||
error_message, balance = await ledger.lightning.status()
|
||||
if error_message:
|
||||
logger.warning(
|
||||
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
f"The backend for {ledger.lightning.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
logger.info(f"Lightning balance: {balance} sat")
|
||||
|
||||
@@ -173,7 +173,7 @@ class LedgerAPI:
|
||||
async def _get_keysets(self, url):
|
||||
resp = self.s.get(
|
||||
url + "/keysets",
|
||||
).json()
|
||||
)
|
||||
resp.raise_for_status()
|
||||
keysets = resp.json()
|
||||
assert len(keysets), Exception("did not receive any keysets")
|
||||
@@ -273,9 +273,16 @@ class LedgerAPI:
|
||||
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
|
||||
"""
|
||||
payload = CheckRequest(proofs=proofs)
|
||||
|
||||
def _check_spendable_include_fields(proofs):
|
||||
"""strips away fields from the model that aren't necessary for the /split"""
|
||||
return {
|
||||
"proofs": {i: {"secret"} for i in range(len(proofs))},
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
self.url + "/check",
|
||||
json=payload.dict(),
|
||||
json=payload.dict(include=_check_spendable_include_fields(proofs)),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return_dict = resp.json()
|
||||
|
||||
19
poetry.lock
generated
19
poetry.lock
generated
@@ -168,7 +168,7 @@ starlette = "0.19.1"
|
||||
all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"]
|
||||
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
|
||||
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.3,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
|
||||
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
@@ -553,17 +553,6 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""
|
||||
[package.extras]
|
||||
full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "starlette-context"
|
||||
version = "0.3.4"
|
||||
description = "Access context in Starlette"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
starlette = "*"
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
@@ -643,7 +632,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "14ff9c57ca971c645f1a075b5c6fa0a84a38eaf6399d14afa724136728a3da03"
|
||||
content-hash = "b4e980ee90226bab07750b1becc8c69df7752f6d168d200a79c782aa1efe61da"
|
||||
|
||||
[metadata.files]
|
||||
anyio = [
|
||||
@@ -1010,10 +999,6 @@ starlette = [
|
||||
{file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"},
|
||||
{file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"},
|
||||
]
|
||||
starlette-context = [
|
||||
{file = "starlette_context-0.3.4-py37-none-any.whl", hash = "sha256:b16bf17bd3ead7ded2f458aebf7f913744b9cf28305e16c69b435a6c6ddf1135"},
|
||||
{file = "starlette_context-0.3.4.tar.gz", hash = "sha256:2d28e1838302fb5d5adacadc10fb73fb2d5cca1f0aa1e279698701cc96f1567c"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.3.3"
|
||||
version = "0.4.0"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
@@ -22,7 +22,6 @@ bitstring = "^3.1.9"
|
||||
secp256k1 = "^0.14.0"
|
||||
sqlalchemy-aio = "^0.17.0"
|
||||
python-bitcoinlib = "^0.11.2"
|
||||
starlette-context = "^0.3.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = {version = "^22.8.0", allow-prereleases = true}
|
||||
|
||||
@@ -34,7 +34,6 @@ six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette-context==0.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typing-extensions==4.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.3.3",
|
||||
version="0.4.0",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
119
tests/test_mint.py
Normal file
119
tests/test_mint.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from cashu.core.base import BlindedMessage, Proof
|
||||
from cashu.core.helpers import async_unwrap, sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
|
||||
SERVER_ENDPOINT = "http://localhost:3338"
|
||||
|
||||
import os
|
||||
|
||||
from cashu.core.db import Database
|
||||
from cashu.core.settings import MAX_ORDER, MINT_PRIVATE_KEY
|
||||
from cashu.lightning.lnbits import LNbitsWallet
|
||||
from cashu.mint import migrations
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
|
||||
async def assert_err(f, msg):
|
||||
"""Compute f() and expect an error message 'msg'."""
|
||||
try:
|
||||
await f
|
||||
except Exception as exc:
|
||||
assert exc.args[0] == msg, Exception(
|
||||
f"Expected error: {msg}, got: {exc.args[0]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_amt(proofs: List[Proof], expected: int):
|
||||
"""Assert amounts the proofs contain."""
|
||||
assert [p.amount for p in proofs] == expected
|
||||
|
||||
|
||||
async def start_mint_init(ledger):
|
||||
await migrate_databases(ledger.db, migrations)
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def ledger():
|
||||
db_file = "data/mint/test.sqlite3"
|
||||
if os.path.exists(db_file):
|
||||
os.remove(db_file)
|
||||
ledger = Ledger(
|
||||
db=Database("test", "data/mint"),
|
||||
seed="TEST_PRIVATE_KEY",
|
||||
derivation_path="0/0/0/0",
|
||||
lightning=None,
|
||||
)
|
||||
await start_mint_init(ledger)
|
||||
yield ledger
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keysets(ledger: Ledger):
|
||||
assert len(ledger.keysets.keysets)
|
||||
assert len(ledger.keysets.get_ids())
|
||||
assert ledger.keyset.id == "XQM1wwtQbOXE"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keyset(ledger: Ledger):
|
||||
keyset = ledger.get_keyset()
|
||||
assert type(keyset) == dict
|
||||
assert len(keyset) == MAX_ORDER
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
)
|
||||
]
|
||||
promises = await ledger.mint(blinded_messages_mock)
|
||||
assert len(promises)
|
||||
assert promises[0].amount == 8
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint_invalid_blinded_message(ledger: Ledger):
|
||||
blinded_messages_mock_invalid_key = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bff02d7fa2365379b0840afe249a7a9d71237",
|
||||
)
|
||||
]
|
||||
await assert_err(
|
||||
ledger.mint(blinded_messages_mock_invalid_key), "invalid public key"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_promises(ledger: Ledger):
|
||||
blinded_messages_mock = [
|
||||
BlindedMessage(
|
||||
amount=8,
|
||||
B_="02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239",
|
||||
)
|
||||
]
|
||||
promises = await ledger._generate_promises(blinded_messages_mock)
|
||||
assert (
|
||||
promises[0].C_
|
||||
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_output_split(ledger: Ledger):
|
||||
assert ledger._get_output_split(13) == [1, 4, 8]
|
||||
@@ -7,6 +7,7 @@ import pytest_asyncio
|
||||
from cashu.core.base import Proof
|
||||
from cashu.core.helpers import async_unwrap, sum_proofs
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.core.settings import MAX_ORDER
|
||||
from cashu.wallet import migrations
|
||||
from cashu.wallet.wallet import Wallet
|
||||
from cashu.wallet.wallet import Wallet as Wallet1
|
||||
@@ -48,6 +49,23 @@ async def wallet2():
|
||||
yield wallet2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keys(wallet1: Wallet):
|
||||
assert len(wallet1.keys) == MAX_ORDER
|
||||
keyset = await wallet1._get_keys(wallet1.url)
|
||||
assert type(keyset.id) == str
|
||||
assert len(keyset.id) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_keysets(wallet1: Wallet):
|
||||
keyset = await wallet1._get_keysets(wallet1.url)
|
||||
assert type(keyset) == dict
|
||||
assert type(keyset["keysets"]) == list
|
||||
assert len(keyset["keysets"]) > 0
|
||||
assert keyset["keysets"][0] == wallet1.keyset_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mint(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
@@ -63,6 +81,8 @@ async def test_split(wallet1: Wallet):
|
||||
assert [p.amount for p in p1] == [4, 8, 32]
|
||||
assert sum_proofs(p2) == 20
|
||||
assert [p.amount for p in p2] == [4, 16]
|
||||
assert all([p.id == wallet1.keyset_id for p in p1])
|
||||
assert all([p.id == wallet1.keyset_id for p in p2])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user