lnbits-legend/lnbits/core/crud.py

738 lines
19 KiB
Python
Raw Normal View History

import datetime
2022-07-16 14:23:03 +02:00
import json
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
2022-07-16 14:23:03 +02:00
from uuid import uuid4
from lnbits import bolt11
2022-07-16 14:23:03 +02:00
from lnbits.db import COCKROACH, POSTGRES, Connection
2023-01-17 15:28:24 +01:00
from lnbits.extension_manger import InstallableExtension
2022-12-19 09:10:41 +01:00
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
from . import db
from .models import BalanceCheck, Payment, User, Wallet
# accounts
# --------
async def create_account(conn: Optional[Connection] = None) -> User:
user_id = uuid4().hex
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = await get_account(user_id=user_id, conn=conn)
2020-04-26 13:28:19 +02:00
assert new_account, "Newly created account couldn't be retrieved"
return new_account
async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
2022-01-30 20:43:30 +01:00
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
)
return User(**row) if row else None
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
user = await (conn or db).fetchone(
"SELECT id, email FROM accounts WHERE id = ?", (user_id,)
)
if user:
extensions = await (conn or db).fetchall(
2021-06-22 04:22:52 +02:00
"""SELECT extension FROM extensions WHERE "user" = ? AND active""",
(user_id,),
)
wallets = await (conn or db).fetchall(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
2021-06-22 04:22:52 +02:00
WHERE "user" = ?
""",
(user_id,),
)
else:
return None
return User(
2021-10-17 19:33:29 +02:00
id=user["id"],
email=user["email"],
extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets],
admin=user["id"] == settings.super_user
or user["id"] in settings.lnbits_admin_users,
2021-10-17 19:33:29 +02:00
)
# extensions
# -------
async def add_installed_extension(
2023-01-17 15:28:24 +01:00
ext: InstallableExtension,
conn: Optional[Connection] = None,
) -> None:
2023-01-17 15:28:24 +01:00
meta = {
"installed_release": dict(ext.installed_release)
if ext.installed_release
else None,
"dependencies": ext.dependencies,
}
version = ext.installed_release.version if ext.installed_release else ""
await (conn or db).execute(
"""
2023-01-17 15:28:24 +01:00
INSERT INTO installed_extensions (id, version, name, short_description, icon, icon_url, stars, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO
2023-01-18 09:43:57 +01:00
UPDATE SET (version, name, active, short_description, icon, icon_url, stars, meta) = (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
2023-01-17 15:28:24 +01:00
ext.id,
version,
2023-01-17 15:28:24 +01:00
ext.name,
ext.short_description,
ext.icon,
ext.icon_url,
ext.stars,
json.dumps(meta),
version,
2023-01-17 15:28:24 +01:00
ext.name,
2023-01-18 09:43:57 +01:00
False,
2023-01-17 15:28:24 +01:00
ext.short_description,
ext.icon,
ext.icon_url,
ext.stars,
json.dumps(meta),
),
)
async def update_installed_extension_state(
*, ext_id: str, active: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
UPDATE installed_extensions SET active = ? WHERE id = ?
""",
(active, ext_id),
)
async def delete_installed_extension(
*, ext_id: str, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
DELETE from installed_extensions WHERE id = ?
""",
(ext_id,),
)
2023-01-17 13:51:09 +01:00
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
row = await (conn or db).fetchone(
"SELECT * FROM installed_extensions WHERE id = ?",
(ext_id,),
)
if not row:
return None
2023-01-17 13:51:09 +01:00
return dict(row)
2023-01-17 15:28:24 +01:00
async def get_installed_extensions(
conn: Optional[Connection] = None,
) -> List["InstallableExtension"]:
rows = await (conn or db).fetchall(
"SELECT * FROM installed_extensions",
(),
)
return [InstallableExtension.from_row(row) for row in rows]
async def get_inactive_extensions(*, conn: Optional[Connection] = None) -> List[str]:
inactive_extensions = await (conn or db).fetchall(
"""SELECT id FROM installed_extensions WHERE NOT active""",
(),
)
return (
[ext[0] for ext in inactive_extensions] if len(inactive_extensions) != 0 else []
)
async def update_user_extension(
*, user_id: str, extension: str, active: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
INSERT INTO extensions ("user", extension, active) VALUES (?, ?, ?)
ON CONFLICT ("user", extension) DO UPDATE SET active = ?
""",
(user_id, extension, active, active),
)
# wallets
# -------
async def create_wallet(
*,
user_id: str,
wallet_name: Optional[str] = None,
conn: Optional[Connection] = None,
) -> Wallet:
wallet_id = uuid4().hex
await (conn or db).execute(
"""
2021-06-22 04:22:52 +02:00
INSERT INTO wallets (id, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
""",
(
wallet_id,
wallet_name or settings.lnbits_default_wallet_name,
user_id,
uuid4().hex,
uuid4().hex,
),
)
new_wallet = await get_wallet(wallet_id=wallet_id, conn=conn)
2020-04-26 13:28:19 +02:00
assert new_wallet, "Newly created wallet couldn't be retrieved"
return new_wallet
2021-10-17 19:33:29 +02:00
2021-08-06 12:15:07 +02:00
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
2022-07-19 18:51:35 +02:00
return await (conn or db).execute(
2021-08-06 12:15:07 +02:00
"""
UPDATE wallets SET
name = ?
WHERE id = ?
""",
2021-10-17 19:33:29 +02:00
(new_name, wallet_id),
2021-08-06 12:15:07 +02:00
)
async def delete_wallet(
*, user_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
UPDATE wallets AS w
SET
2021-06-22 04:22:52 +02:00
"user" = 'del:' || w."user",
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
2021-06-22 04:22:52 +02:00
WHERE id = ? AND "user" = ?
""",
(wallet_id, user_id),
)
async def get_wallet(
wallet_id: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
row = await (conn or db).fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE id = ?
""",
(wallet_id,),
)
return Wallet(**row) if row else None
async def get_wallet_for_key(
key: str, key_type: str = "invoice", conn: Optional[Connection] = None
) -> Optional[Wallet]:
row = await (conn or db).fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE adminkey = ? OR inkey = ?
""",
(key, key),
)
if not row:
return None
if key_type == "admin" and row["adminkey"] != key:
return None
return Wallet(**row)
async def get_total_balance(conn: Optional[Connection] = None):
row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances")
return 0 if row[0] is None else row[0]
# wallet payments
# ---------------
async def get_standalone_payment(
checking_id_or_hash: str,
conn: Optional[Connection] = None,
incoming: Optional[bool] = False,
wallet_id: Optional[str] = None,
) -> Optional[Payment]:
clause: str = "checking_id = ? OR hash = ?"
values = [checking_id_or_hash, checking_id_or_hash]
if incoming:
clause = f"({clause}) AND amount > 0"
if wallet_id:
clause = f"({clause}) AND wallet = ?"
values.append(wallet_id)
row = await (conn or db).fetchone(
f"""
2020-09-28 04:12:55 +02:00
SELECT *
FROM apipayments
WHERE {clause}
2021-03-07 04:08:36 +01:00
LIMIT 1
2020-09-28 04:12:55 +02:00
""",
tuple(values),
2020-09-28 04:12:55 +02:00
)
return Payment.from_row(row) if row else None
async def get_wallet_payment(
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
) -> Optional[Payment]:
row = await (conn or db).fetchone(
"""
SELECT *
FROM apipayments
WHERE wallet = ? AND hash = ?
""",
(wallet_id, payment_hash),
)
return Payment.from_row(row) if row else None
async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5):
rows = await db.fetchall(
f"""
SELECT * FROM apipayments
WHERE pending = 'false'
AND extra LIKE ?
AND extra LIKE ?
ORDER BY time DESC LIMIT {limit}
""",
(
f"%{ext_name}%",
f"%{ext_id}%",
),
)
return rows
async def get_payments(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
args: List[Any] = []
clause: List[str] = []
if since != None:
2021-06-22 04:22:52 +02:00
if db.type == POSTGRES:
clause.append("time > to_timestamp(?)")
2021-07-02 23:32:58 +02:00
elif db.type == COCKROACH:
clause.append("time > cast(? AS timestamp)")
2021-06-22 04:22:52 +02:00
else:
clause.append("time > ?")
args.append(since)
if wallet_id:
clause.append("wallet = ?")
args.append(wallet_id)
if complete and pending:
pass
elif complete:
2021-06-22 04:22:52 +02:00
clause.append("((amount > 0 AND pending = false) OR amount < 0)")
elif pending:
2021-06-22 04:22:52 +02:00
clause.append("pending = true")
else:
pass
if outgoing and incoming:
pass
elif outgoing:
clause.append("amount < 0")
elif incoming:
clause.append("amount > 0")
else:
pass
if exclude_uncheckable: # checkable means it has a checking_id that isn't internal
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{where}
ORDER BY time DESC
{limit_offset_clause}
""",
tuple(args),
)
return [Payment.from_row(row) for row in rows]
2022-01-31 17:29:42 +01:00
async def delete_expired_invoices(
conn: Optional[Connection] = None,
) -> None:
2021-05-14 18:12:05 +02:00
# first we delete all invoices older than one month
await (conn or db).execute(
2021-06-22 04:22:52 +02:00
f"""
2021-05-14 18:12:05 +02:00
DELETE FROM apipayments
2021-06-22 04:22:52 +02:00
WHERE pending = true AND amount > 0
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
2021-05-14 18:12:05 +02:00
"""
)
2022-10-05 14:17:23 +02:00
# then we delete all invoices whose expiry date is in the past
await (conn or db).execute(
2021-06-22 04:22:52 +02:00
f"""
2022-10-05 14:17:23 +02:00
DELETE FROM apipayments
WHERE pending = true AND amount > 0
AND expiry < {db.timestamp_now}
2021-05-14 18:12:05 +02:00
"""
)
2020-04-17 21:13:57 +02:00
# payments
# --------
async def create_payment(
*,
wallet_id: str,
checking_id: str,
payment_request: str,
payment_hash: str,
amount: int,
memo: str,
fee: int = 0,
preimage: Optional[str] = None,
pending: bool = True,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
conn: Optional[Connection] = None,
) -> Payment:
# todo: add this when tests are fixed
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
# assert previous_payment is None, "Payment already exists"
2022-10-05 14:17:23 +02:00
try:
invoice = bolt11.decode(payment_request)
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
except:
2022-12-06 10:48:16 +01:00
# assume maximum bolt11 expiry of 31 days to be on the safe side
2022-10-05 14:17:23 +02:00
expiration_date = datetime.datetime.now() + datetime.timedelta(days=31)
await (conn or db).execute(
"""
INSERT INTO apipayments
(wallet, checking_id, bolt11, hash, preimage,
2022-10-05 14:17:23 +02:00
amount, pending, memo, fee, extra, webhook, expiry)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
wallet_id,
checking_id,
payment_request,
payment_hash,
preimage,
amount,
2021-06-22 04:22:52 +02:00
pending,
memo,
fee,
json.dumps(extra)
if extra and extra != {} and type(extra) is dict
else None,
webhook,
2022-12-02 17:38:36 +01:00
db.datetime_to_timestamp(expiration_date),
),
)
new_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
2020-04-26 13:28:19 +02:00
assert new_payment, "Newly created payment couldn't be retrieved"
return new_payment
async def update_payment_status(
2021-10-17 19:33:29 +02:00
checking_id: str, pending: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
2021-03-07 23:18:02 +01:00
"UPDATE apipayments SET pending = ? WHERE checking_id = ?",
2021-10-17 19:33:29 +02:00
(pending, checking_id),
)
async def update_payment_details(
checking_id: str,
pending: Optional[bool] = None,
fee: Optional[int] = None,
preimage: Optional[str] = None,
new_checking_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> None:
set_clause: List[str] = []
set_variables: List[Any] = []
if new_checking_id is not None:
set_clause.append("checking_id = ?")
set_variables.append(new_checking_id)
if pending is not None:
set_clause.append("pending = ?")
set_variables.append(pending)
if fee is not None:
set_clause.append("fee = ?")
set_variables.append(fee)
if preimage is not None:
set_clause.append("preimage = ?")
set_variables.append(preimage)
set_variables.append(checking_id)
await (conn or db).execute(
f"UPDATE apipayments SET {', '.join(set_clause)} WHERE checking_id = ?",
tuple(set_variables),
)
return
async def update_payment_extra(
payment_hash: str,
extra: dict,
outgoing: bool = False,
conn: Optional[Connection] = None,
) -> None:
"""
Only update the `extra` field for the payment.
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
"""
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
row = await (conn or db).fetchone(
f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}",
(payment_hash,),
)
if not row:
return
2022-12-21 14:47:22 +01:00
db_extra = json.loads(row["extra"] if row["extra"] else "{}")
db_extra.update(extra)
await (conn or db).execute(
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
(json.dumps(db_extra), payment_hash),
)
2021-10-17 19:33:29 +02:00
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
)
async def delete_wallet_payment(
checking_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = ? AND wallet = ?",
(checking_id, wallet_id),
)
2020-08-19 18:53:27 +02:00
async def check_internal(
2021-10-17 19:33:29 +02:00
payment_hash: str, conn: Optional[Connection] = None
) -> Optional[str]:
row = await (conn or db).fetchone(
"""
2020-11-19 18:23:35 +01:00
SELECT checking_id FROM apipayments
2021-08-06 12:15:07 +02:00
WHERE hash = ? AND pending AND amount > 0
2020-11-19 18:23:35 +01:00
""",
(payment_hash,),
)
if not row:
return None
else:
return row["checking_id"]
2021-04-17 23:27:15 +02:00
# balance_check
# -------------
async def save_balance_check(
2021-10-17 19:33:29 +02:00
wallet_id: str, url: str, conn: Optional[Connection] = None
2021-04-17 23:27:15 +02:00
):
domain = urlparse(url).netloc
await (conn or db).execute(
"""
2021-06-22 04:22:52 +02:00
INSERT INTO balance_check (wallet, service, url) VALUES (?, ?, ?)
ON CONFLICT (wallet, service) DO UPDATE SET url = ?
2021-04-17 23:27:15 +02:00
""",
2021-06-22 04:22:52 +02:00
(wallet_id, domain, url, url),
2021-04-17 23:27:15 +02:00
)
async def get_balance_check(
2021-10-17 19:33:29 +02:00
wallet_id: str, domain: str, conn: Optional[Connection] = None
2021-04-17 23:27:15 +02:00
) -> Optional[BalanceCheck]:
row = await (conn or db).fetchone(
"""
SELECT wallet, service, url
FROM balance_check
WHERE wallet = ? AND service = ?
""",
(wallet_id, domain),
)
return BalanceCheck.from_row(row) if row else None
async def get_balance_checks(conn: Optional[Connection] = None) -> List[BalanceCheck]:
rows = await (conn or db).fetchall("SELECT wallet, service, url FROM balance_check")
return [BalanceCheck.from_row(row) for row in rows]
# balance_notify
# --------------
async def save_balance_notify(
2021-10-17 19:33:29 +02:00
wallet_id: str, url: str, conn: Optional[Connection] = None
2021-04-17 23:27:15 +02:00
):
await (conn or db).execute(
"""
2021-06-22 04:22:52 +02:00
INSERT INTO balance_notify (wallet, url) VALUES (?, ?)
ON CONFLICT (wallet) DO UPDATE SET url = ?
2021-04-17 23:27:15 +02:00
""",
2021-06-22 04:22:52 +02:00
(wallet_id, url, url),
2021-04-17 23:27:15 +02:00
)
async def get_balance_notify(
2021-10-17 19:33:29 +02:00
wallet_id: str, conn: Optional[Connection] = None
2021-04-17 23:27:15 +02:00
) -> Optional[str]:
row = await (conn or db).fetchone(
"""
SELECT url
FROM balance_notify
WHERE wallet = ?
""",
(wallet_id,),
)
return row[0] if row else None
# admin
# --------
async def get_super_settings() -> Optional[SuperSettings]:
row = await db.fetchone("SELECT * FROM settings")
if not row:
return None
2022-12-09 12:14:22 +01:00
editable_settings = json.loads(row["editable_settings"])
return SuperSettings(**{"super_user": row["super_user"], **editable_settings})
async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSettings]:
sets = await get_super_settings()
if not sets:
return None
row_dict = dict(sets)
row_dict.pop("super_user")
admin_settings = AdminSettings(
super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
**row_dict,
)
return admin_settings
async def delete_admin_settings():
await db.execute("DELETE FROM settings")
2022-12-19 09:10:41 +01:00
async def update_admin_settings(data: EditableSettings):
await db.execute(f"UPDATE settings SET editable_settings = ?", (json.dumps(data),))
async def update_super_user(super_user: str):
2022-12-16 11:14:00 +01:00
await db.execute("UPDATE settings SET super_user = ?", (super_user,))
return await get_super_settings()
async def create_admin_settings(super_user: str, new_settings: dict):
2022-12-09 12:14:22 +01:00
sql = f"INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
await db.execute(sql, (super_user, json.dumps(new_settings)))
return await get_super_settings()
# db versions
# --------------
async def get_dbversions(conn: Optional[Connection] = None):
rows = await (conn or db).fetchall("SELECT * FROM dbversions")
return {row["db"]: row["version"] for row in rows}
async def update_migration_version(conn, db_name, version):
await (conn or db).execute(
"""
INSERT INTO dbversions (db, version) VALUES (?, ?)
ON CONFLICT (db) DO UPDATE SET version = ?
""",
(db_name, version, version),
)