diff --git a/cashu/core/base.py b/cashu/core/base.py index 7db5eb2..5a94360 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -12,15 +12,6 @@ class P2SHScript(BaseModel): signature: str address: Union[str, None] = None - @classmethod - def from_row(cls, row: Row): - return cls( - address=row[0], - script=row[1], - signature=row[2], - used=row[3], - ) - class Proof(BaseModel): id: str = "" @@ -28,36 +19,10 @@ class Proof(BaseModel): secret: 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 - time_created: str = "" - time_reserved: str = "" - - @classmethod - def from_row(cls, row: Row): - return cls( - amount=row[0], - C=row[1], - secret=row[2], - reserved=row[3] or False, - send_id=row[4] or "", - time_created=row[5] or "", - time_reserved=row[6] or "", - id=row[7] or "", - ) - - @classmethod - def from_dict(cls, d: dict): - assert "amount" in d, "no amount in proof" - return cls( - amount=d.get("amount"), - C=d.get("C"), - secret=d.get("secret") or "", - reserved=d.get("reserved") or False, - send_id=d.get("send_id") or "", - time_created=d.get("time_created") or "", - time_reserved=d.get("time_reserved") or "", - ) + reserved: Union[None, bool] = False # whether this proof is reserved for sending + send_id: Union[None, str] = "" # unique ID of send attempt + time_created: Union[None, str] = "" + time_reserved: Union[None, str] = "" def to_dict(self): return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C) @@ -81,17 +46,12 @@ class Proofs(BaseModel): class Invoice(BaseModel): amount: int pr: str - hash: str - issued: bool = False - - @classmethod - def from_row(cls, row: Row): - return cls( - amount=int(row[0]), - pr=str(row[1]), - hash=str(row[2]), - issued=bool(row[3]), - ) + hash: Union[None, str] = None + preimage: Union[str, None] = None + issued: Union[None, bool] = False + paid: Union[None, bool] = False + time_created: Union[None, str, int, float] = "" + time_paid: Union[None, str, int, float] = "" class BlindedMessage(BaseModel): @@ -104,14 +64,6 @@ class BlindedSignature(BaseModel): amount: int C_: str - @classmethod - def from_dict(cls, d: dict): - return cls( - id=d.get("id"), - amount=d["amount"], - C_=d["C_"], - ) - class MintRequest(BaseModel): blinded_messages: List[BlindedMessage] = [] @@ -173,16 +125,6 @@ class KeyBase(BaseModel): amount: int pubkey: str - @classmethod - def from_row(cls, row: Row): - if row is None: - return cls - return cls( - id=row[0], - amount=int(row[1]), - pubkey=row[2], - ) - class WalletKeyset: id: str @@ -213,19 +155,6 @@ class WalletKeyset: self.public_keys = pubkeys self.id = derive_keyset_id(self.public_keys) - @classmethod - def from_row(cls, row: Row): - if row is None: - return cls - return cls( - id=row[0], - mint_url=row[1], - valid_from=row[2], - valid_to=row[3], - first_seen=row[4], - active=row[5], - ) - class MintKeyset: id: str @@ -266,20 +195,6 @@ class MintKeyset: self.public_keys = derive_pubkeys(self.private_keys) self.id = derive_keyset_id(self.public_keys) - @classmethod - def from_row(cls, row: Row): - if row is None: - return cls - return cls( - id=row[0], - derivation_path=row[1], - valid_from=row[2], - valid_to=row[3], - first_seen=row[4], - active=row[5], - version=row[6], - ) - def get_keybase(self): return { k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 6ce7120..e07d2f2 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -135,7 +135,7 @@ async def get_lightning_invoice( """, (hash,), ) - return Invoice.from_row(row) + return Invoice(**row) async def update_lightning_invoice( @@ -204,4 +204,4 @@ async def get_keyset( """, tuple(values), ) - return [MintKeyset.from_row(row) for row in rows] + return [MintKeyset(**row) for row in rows] diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 9e906dc..719e02a 100755 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -85,14 +85,14 @@ async def invoice(ctx, amount: int, hash: str): if not LIGHTNING: r = await wallet.mint(amount) elif amount and not hash: - r = await wallet.request_mint(amount) - if "pr" in r: + invoice = await wallet.request_mint(amount) + if invoice.pr: print(f"Pay invoice to mint {amount} sat:") print("") - print(f"Invoice: {r['pr']}") + print(f"Invoice: {invoice.pr}") print("") print( - f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {r['hash']}" + f"Execute this command if you abort the check:\ncashu invoice {amount} --hash {invoice.hash}" ) check_until = time.time() + 5 * 60 # check for five minutes print("") @@ -105,7 +105,7 @@ async def invoice(ctx, amount: int, hash: str): while time.time() < check_until and not paid: time.sleep(3) try: - await wallet.mint(amount, r["hash"]) + await wallet.mint(amount, invoice.hash) paid = True print(" Invoice paid.") except Exception as e: @@ -221,7 +221,7 @@ async def receive(ctx, coin: str, lock: str): signature = p2shscripts[0].signature else: script, signature = None, None - proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin))] + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))] _, _ = await wallet.redeem(proofs, scnd_script=script, scnd_siganture=signature) wallet.status() @@ -250,9 +250,7 @@ async def burn(ctx, coin: str, all: bool, force: bool): proofs = wallet.proofs else: # check only the specified ones - proofs = [ - Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(coin)) - ] + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(coin))] wallet.status() await wallet.invalidate(proofs) wallet.status() diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index d6f8c47..ed0f3b4 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -1,7 +1,7 @@ import time from typing import Any, List, Optional -from cashu.core.base import KeyBase, P2SHScript, Proof, WalletKeyset +from cashu.core.base import Invoice, KeyBase, P2SHScript, Proof, WalletKeyset from cashu.core.db import Connection, Database @@ -31,7 +31,7 @@ async def get_proofs( SELECT * from proofs """ ) - return [Proof.from_row(r) for r in rows] + return [Proof(**dict(r)) for r in rows] async def get_reserved_proofs( @@ -45,7 +45,7 @@ async def get_reserved_proofs( WHERE reserved """ ) - return [Proof.from_row(r) for r in rows] + return [Proof(**r) for r in rows] async def invalidate_proof( @@ -162,7 +162,7 @@ async def get_unused_locks( """, tuple(args), ) - return [P2SHScript.from_row(r) for r in rows] + return [P2SHScript(**r) for r in rows] async def update_p2sh_used( @@ -233,4 +233,78 @@ async def get_keyset( """, tuple(values), ) - return WalletKeyset.from_row(row) if row is not None else None + return WalletKeyset(**row) if row is not None else None + + +async def store_lightning_invoice( + db: Database, + invoice: Invoice, + conn: Optional[Connection] = None, +): + + await (conn or db).execute( + f""" + INSERT INTO invoices + (amount, pr, hash, preimage, paid, time_created, time_paid) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + invoice.amount, + invoice.pr, + invoice.hash, + invoice.preimage, + invoice.paid, + invoice.time_created, + invoice.time_paid, + ), + ) + + +async def get_lightning_invoice( + db: Database, + hash: str = None, + conn: Optional[Connection] = None, +): + clauses = [] + values: List[Any] = [] + if hash: + clauses.append("hash = ?") + values.append(hash) + + where = "" + if clauses: + where = f"WHERE {' AND '.join(clauses)}" + + row = await (conn or db).fetchone( + f""" + SELECT * from invoices + {where} + """, + (hash,), + ) + return Invoice(**row) + + +async def update_lightning_invoice( + db: Database, + hash: str, + paid: bool, + time_paid: int = None, + conn: Optional[Connection] = None, +): + clauses = [] + values: List[Any] = [] + clauses.append("paid = ?") + values.append(paid) + + if time_paid: + clauses.append("time_paid = ?") + values.append(time_paid) + + await (conn or db).execute( + f"UPDATE invoices SET {', '.join(clauses)} WHERE hash = ?", + ( + *values, + hash, + ), + ) diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 0a0a73e..2bf9a3a 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -119,18 +119,28 @@ async def m005_wallet_keysets(db: Database): ); """ ) - # await db.execute( - # f""" - # CREATE TABLE IF NOT EXISTS mint_pubkeys ( - # id TEXT NOT NULL, - # amount INTEGER NOT NULL, - # pubkey TEXT NOT NULL, - - # UNIQUE (id, pubkey) - - # ); - # """ - # ) await db.execute("ALTER TABLE proofs ADD COLUMN id TEXT") await db.execute("ALTER TABLE proofs_used ADD COLUMN id TEXT") + + +async def m006_invoices(db: Database): + """ + Stores Lightning invoices. + """ + await db.execute( + f""" + CREATE TABLE IF NOT EXISTS invoices ( + amount INTEGER NOT NULL, + pr TEXT NOT NULL, + hash TEXT, + preimage TEXT, + paid BOOL DEFAULT FALSE, + time_created TIMESTAMP DEFAULT {db.timestamp_now}, + time_paid TIMESTAMP DEFAULT {db.timestamp_now}, + + UNIQUE (hash) + + ); + """ + ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index aacf92d..8c55e68 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -1,6 +1,7 @@ import base64 import json import secrets as scrts +import time import uuid from itertools import groupby from typing import Dict, List @@ -14,6 +15,7 @@ from cashu.core.base import ( BlindedSignature, CheckFeesRequest, CheckRequest, + Invoice, MeltRequest, MintRequest, P2SHScript, @@ -38,8 +40,10 @@ from cashu.wallet.crud import ( invalidate_proof, secret_used, store_keyset, + store_lightning_invoice, store_p2sh, store_proof, + update_lightning_invoice, update_proof_reserved, ) @@ -175,7 +179,7 @@ class LedgerAPI: resp.raise_for_status() return_dict = resp.json() self.raise_on_error(return_dict) - return return_dict + return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"]) async def mint(self, amounts, payment_hash=None): """Mints new coins and returns a proof of promise.""" @@ -192,7 +196,7 @@ class LedgerAPI: promises_list = resp.json() self.raise_on_error(promises_list) - promises = [BlindedSignature.from_dict(p) for p in promises_list] + promises = [BlindedSignature(**p) for p in promises_list] return self._construct_proofs(promises, secrets, rs) async def split(self, proofs, amount, scnd_secret: str = None): @@ -246,8 +250,8 @@ class LedgerAPI: promises_dict = resp.json() self.raise_on_error(promises_dict) - promises_fst = [BlindedSignature.from_dict(p) for p in promises_dict["fst"]] - promises_snd = [BlindedSignature.from_dict(p) for p in promises_dict["snd"]] + promises_fst = [BlindedSignature(**p) for p in promises_dict["fst"]] + promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]] # Construct proofs from promises (i.e., unblind signatures) frst_proofs = self._construct_proofs( promises_fst, secrets[: len(promises_fst)], rs[: len(promises_fst)] @@ -340,7 +344,10 @@ class Wallet(LedgerAPI): return {key: list(group) for key, group in groupby(proofs, lambda p: p.id)} async def request_mint(self, amount): - return super().request_mint(amount) + invoice = super().request_mint(amount) + invoice.time_created = int(time.time()) + await store_lightning_invoice(db=self.db, invoice=invoice) + return invoice async def mint(self, amount: int, payment_hash: str = None): split = amount_split(amount) @@ -348,6 +355,10 @@ class Wallet(LedgerAPI): if proofs == []: raise Exception("received no proofs.") await self._store_proofs(proofs) + if payment_hash: + await update_lightning_invoice( + db=self.db, hash=payment_hash, paid=True, time_paid=int(time.time()) + ) self.proofs += proofs return proofs @@ -389,6 +400,14 @@ class Wallet(LedgerAPI): status = await super().pay_lightning(proofs, invoice) if status["paid"] == True: await self.invalidate(proofs) + invoice_obj = Invoice( + amount=-sum_proofs(proofs), + pr=invoice, + preimage=status.get("preimage"), + paid=True, + time_paid=time.time(), + ) + await store_lightning_invoice(db=self.db, invoice=invoice_obj) else: raise Exception("could not pay invoice.") return status["paid"]