Files
nutshell/cashu/wallet/crud.py
callebtc a518274f7e Nutshell cleanup wishlist (#332)
* fix keys

* fix tests

* backwards compatible api upgrade

* upgrade seems to work

* fix tests

* add deprecated api functions

* add more tests of backwards compat

* add test serialization for nut00

* remove a redundant test

* move mint and melt to new api

* mypy works

* CI: mypy --check-untyped-defs

* add deprecated router

* add hints and remove logs

* fix tests

* cleanup

* use new mint and melt endpoints

* tests passing?

* fix mypy

* make format

* make format

* make format

* commit

* errors gone

* save

* adjust the API

* store quotes in db

* make mypy happy

* add fakewallet settings

* remove LIGHTNING=True and pass quote id for melt

* format

* tests passing

* add CoreLightningRestWallet

* add macaroon loader

* add correct config

* preimage -> proof

* move wallet.status() to cli.helpers.print_status()

* remove statuses from tests

* remove

* make format

* Use httpx in deprecated wallet

* fix cln interface

* create invoice before quote

* internal transactions and deprecated api testing

* fix tests

* add deprecated API tests

* fastapi type hints break things

* fix duplicate wallet error

* make format

* update poetry in CI to 1.7.1

* precommit restore

* remove bolt11

* oops

* default poetry

* store fee reserve for melt quotes and refactor melt()

* works?

* make format

* test

* finally

* fix deprecated models

* rename v1 endpoints to bolt11

* raise restore and check to v1, bump version to 0.15.0

* add version byte to keyset id

* remove redundant fields in json

* checks

* generate bip32 keyset wip

* migrate old keysets

* load duplicate keys

* duplicate old keysets

* revert router changes

* add deprecated /check and /restore endpoints

* try except invalidate

* parse unit from derivation path, adjust keyset id calculation with bytes

* remove keyest id from functions again and rely on self.keyset_id

* mosts tests work

* mint loads multiple derivation paths

* make format

* properly print units

* fix tests

* wallet works with multiple units

* add strike wallet and choose backend dynamically

* fix mypy

* add get_payment_quote to lightning backends

* make format

* fix startup

* fix lnbitswallet

* fix tests

* LightningWallet -> LightningBackend

* remove comments

* make format

* remove msat conversion

* add Amount type

* fix regtest

* use melt_quote as argument for pay_invoice

* test old api

* fees in sats

* fix deprecated fees

* fixes

* print balance correctly

* internally index keyset response by int

* add pydantic validation to input models

* add timestamps to mint db

* store timestamps for invoices, promises, proofs_used

* fix wallet migration

* rotate keys correctly for testing

* remove print

* update latest keyset

* fix tests

* fix test

* make format

* make format with correct black version

* remove nsat and cheese

* test against deprecated mint

* fix tests?

* actually use env var

* mint run with env vars

* moar test

* cleanup

* simplify tests, load all keys

* try out testing with internal invoices

* fix internal melt test

* fix test

* deprecated checkfees expects appropriate fees

* adjust comment

* drop lightning table

* split migration for testing for now, remove it later

* remove unused lightning table

* skip_private_key -> skip_db_read

* throw error on migration error

* reorder

* fix migrations

* fix lnbits fee return value negative

* fix typo

* comments

* add type

* make format

* split must use correct amount

* fix tests

* test deprecated api with internal/external melts

* do not split if not necessary

* refactor

* fix test

* make format with new black

* cleanup and add comments

* add quote state check endpoints

* fix deprecated wallet response

* split -> swap endpoint

* make format

* add expiry to quotes, get quote endpoints, and adjust to nut review comments

* allow overpayment of melt

* add lightning wallet tests

* commiting to save

* fix tests a bit

* make format

* remove comments

* get mint info

* check_spendable default False, and return payment quote checking id

* make format

* bump version in pyproject

* update to /v1/checkstate

* make format

* fix mint api checks

* return witness on /v1/checkstate

* no failfast

* try fail-fast: false in ci.yaml

* fix db lookup

* clean up literals
2024-01-08 00:57:15 +01:00

440 lines
9.9 KiB
Python

import json
import time
from typing import Any, List, Optional, Tuple
from ..core.base import Invoice, Proof, WalletKeyset
from ..core.db import Connection, Database
async def store_proof(
proof: Proof,
db: Database,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"""
INSERT INTO proofs
(id, amount, C, secret, time_created, derivation_path, dleq, mint_id, melt_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
proof.id,
proof.amount,
str(proof.C),
str(proof.secret),
int(time.time()),
proof.derivation_path,
json.dumps(proof.dleq.dict()) if proof.dleq else "",
proof.mint_id,
proof.melt_id,
),
)
async def get_proofs(
*,
db: Database,
melt_id: str = "",
mint_id: str = "",
table: str = "proofs",
conn: Optional[Connection] = None,
):
clauses = []
values: List[Any] = []
if melt_id:
clauses.append("melt_id = ?")
values.append(melt_id)
if mint_id:
clauses.append("mint_id = ?")
values.append(mint_id)
where = ""
if clauses:
where = f"WHERE {' AND '.join(clauses)}"
rows = (
await (conn or db).fetchall(
f"""
SELECT * from {table}
{where}
""",
tuple(values),
),
)
return [Proof.from_dict(dict(r)) for r in rows[0]] if rows else []
async def get_reserved_proofs(
db: Database,
conn: Optional[Connection] = None,
) -> List[Proof]:
rows = await (conn or db).fetchall("""
SELECT * from proofs
WHERE reserved
""")
return [Proof.from_dict(dict(r)) for r in rows]
async def invalidate_proof(
proof: Proof,
db: Database,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"""
DELETE FROM proofs
WHERE secret = ?
""",
(str(proof["secret"]),),
)
await (conn or db).execute(
"""
INSERT INTO proofs_used
(amount, C, secret, time_used, id, derivation_path, mint_id, melt_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
proof.amount,
str(proof.C),
str(proof.secret),
int(time.time()),
proof.id,
proof.derivation_path,
proof.mint_id,
proof.melt_id,
),
)
async def update_proof(
proof: Proof,
*,
reserved: Optional[bool] = None,
send_id: Optional[str] = None,
mint_id: Optional[str] = None,
melt_id: Optional[str] = None,
db: Optional[Database] = None,
conn: Optional[Connection] = None,
) -> None:
clauses = []
values: List[Any] = []
clauses.append("reserved = ?")
values.append(reserved)
if send_id is not None:
clauses.append("send_id = ?")
values.append(send_id)
if reserved is not None:
clauses.append("time_reserved = ?")
values.append(int(time.time()))
if mint_id is not None:
clauses.append("mint_id = ?")
values.append(mint_id)
if melt_id is not None:
clauses.append("melt_id = ?")
values.append(melt_id)
await (conn or db).execute( # type: ignore
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
(*values, str(proof.secret)),
)
async def secret_used(
secret: str,
db: Database,
conn: Optional[Connection] = None,
) -> bool:
rows = await (conn or db).fetchone(
"""
SELECT * from proofs
WHERE secret = ?
""",
(secret,),
)
return rows is not None
async def store_keyset(
keyset: WalletKeyset,
mint_url: str = "",
db: Optional[Database] = None,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute( # type: ignore
"""
INSERT INTO keysets
(id, mint_url, valid_from, valid_to, first_seen, active, public_keys, unit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
keyset.id,
mint_url or keyset.mint_url,
keyset.valid_from or int(time.time()),
keyset.valid_to or int(time.time()),
keyset.first_seen or int(time.time()),
keyset.active,
keyset.serialize(),
keyset.unit.name,
),
)
async def get_keysets(
id: str = "",
mint_url: str = "",
db: Optional[Database] = None,
conn: Optional[Connection] = None,
) -> List[WalletKeyset]:
clauses = []
values: List[Any] = []
clauses.append("active = ?")
values.append(True)
if id:
clauses.append("id = ?")
values.append(id)
if mint_url:
clauses.append("mint_url = ?")
values.append(mint_url)
where = ""
if clauses:
where = f"WHERE {' AND '.join(clauses)}"
row = await (conn or db).fetchall( # type: ignore
f"""
SELECT * from keysets
{where}
""",
tuple(values),
)
ret = []
for r in row:
keyset = WalletKeyset.from_row(r)
ret.append(keyset)
return ret
async def store_lightning_invoice(
db: Database,
invoice: Invoice,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"""
INSERT INTO invoices
(amount, bolt11, id, payment_hash, preimage, paid, time_created, time_paid, out)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
invoice.amount,
invoice.bolt11,
invoice.id,
invoice.payment_hash,
invoice.preimage,
invoice.paid,
invoice.time_created,
invoice.time_paid,
invoice.out,
),
)
async def get_lightning_invoice(
*,
db: Database,
id: str = "",
payment_hash: str = "",
out: Optional[bool] = None,
conn: Optional[Connection] = None,
) -> Optional[Invoice]:
clauses = []
values: List[Any] = []
if id:
clauses.append("id = ?")
values.append(id)
if payment_hash:
clauses.append("payment_hash = ?")
values.append(payment_hash)
if out is not None:
clauses.append("out = ?")
values.append(out)
where = ""
if clauses:
where = f"WHERE {' AND '.join(clauses)}"
query = f"""
SELECT * from invoices
{where}
"""
row = await (conn or db).fetchone(
query,
tuple(values),
)
return Invoice(**row) if row else None
async def get_lightning_invoices(
db: Database,
paid: Optional[bool] = None,
conn: Optional[Connection] = None,
) -> List[Invoice]:
clauses: List[Any] = []
values: List[Any] = []
if paid is not None:
clauses.append("paid = ?")
values.append(paid)
where = ""
if clauses:
where = f"WHERE {' AND '.join(clauses)}"
rows = await (conn or db).fetchall(
f"""
SELECT * from invoices
{where}
""",
tuple(values),
)
return [Invoice(**r) for r in rows]
async def update_lightning_invoice(
db: Database,
id: str,
paid: bool,
time_paid: Optional[int] = None,
preimage: Optional[str] = None,
conn: Optional[Connection] = None,
) -> None:
clauses = []
values: List[Any] = []
clauses.append("paid = ?")
values.append(paid)
if time_paid:
clauses.append("time_paid = ?")
values.append(time_paid)
if preimage:
clauses.append("preimage = ?")
values.append(preimage)
await (conn or db).execute(
f"UPDATE invoices SET {', '.join(clauses)} WHERE id = ?",
(
*values,
id,
),
)
async def bump_secret_derivation(
db: Database,
keyset_id: str,
by: int = 1,
skip: bool = False,
conn: Optional[Connection] = None,
) -> int:
rows = await (conn or db).fetchone(
"SELECT counter from keysets WHERE id = ?", (keyset_id,)
)
# if no counter for this keyset, create one
if not rows:
await (conn or db).execute(
"UPDATE keysets SET counter = ? WHERE id = ?",
(
0,
keyset_id,
),
)
counter = 0
else:
counter = int(rows[0])
if not skip:
await (conn or db).execute(
f"UPDATE keysets SET counter = counter + {by} WHERE id = ?",
(keyset_id,),
)
return counter
async def set_secret_derivation(
db: Database,
keyset_id: str,
counter: int,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"UPDATE keysets SET counter = ? WHERE id = ?",
(
counter,
keyset_id,
),
)
async def set_nostr_last_check_timestamp(
db: Database,
timestamp: int,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"UPDATE nostr SET last = ? WHERE type = ?",
(timestamp, "dm"),
)
async def get_nostr_last_check_timestamp(
db: Database,
conn: Optional[Connection] = None,
) -> Optional[int]:
row = await (conn or db).fetchone(
"""
SELECT last from nostr WHERE type = ?
""",
("dm",),
)
return row[0] if row else None
async def get_seed_and_mnemonic(
db: Database,
conn: Optional[Connection] = None,
) -> Optional[Tuple[str, str]]:
row = await (conn or db).fetchone(
"""
SELECT seed, mnemonic from seed
""",
)
return (
(
row[0],
row[1],
)
if row
else None
)
async def store_seed_and_mnemonic(
db: Database,
seed: str,
mnemonic: str,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"""
INSERT INTO seed
(seed, mnemonic)
VALUES (?, ?)
""",
(
seed,
mnemonic,
),
)