lnbits-legend/lnbits/core/models.py

361 lines
8.8 KiB
Python
Raw Normal View History

2022-12-02 18:51:52 +01:00
import datetime
import hashlib
2022-07-16 14:23:03 +02:00
import hmac
import json
2022-12-02 18:51:18 +01:00
import time
from dataclasses import dataclass
from enum import Enum
from sqlite3 import Row
from typing import Callable, Dict, List, Optional
from ecdsa import SECP256k1, SigningKey
from fastapi import Query
from lnurl import encode as lnurl_encode
from loguru import logger
2022-12-27 14:50:42 +01:00
from pydantic import BaseModel
from lnbits.db import Connection, FilterModel, FromRowModel
2022-07-16 14:23:03 +02:00
from lnbits.helpers import url_for
from lnbits.settings import settings
from lnbits.wallets import get_wallet_class
from lnbits.wallets.base import PaymentStatus
2020-09-27 23:12:55 -03:00
2021-08-20 21:31:01 +01:00
class Wallet(BaseModel):
id: str
name: str
user: str
adminkey: str
inkey: str
currency: Optional[str]
balance_msat: int
deleted: bool
@property
def balance(self) -> int:
return self.balance_msat // 1000
2021-04-17 18:27:15 -03:00
@property
def withdrawable_balance(self) -> int:
from .services import fee_reserve
return self.balance_msat - fee_reserve(self.balance_msat)
@property
def lnurlwithdraw_full(self) -> str:
2021-10-17 18:33:29 +01:00
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
try:
return lnurl_encode(url)
except Exception:
return ""
2021-04-17 18:27:15 -03:00
def lnurlauth_key(self, domain: str) -> SigningKey:
hashing_key = hashlib.sha256(self.id.encode()).digest()
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
2020-12-31 18:50:16 +01:00
return SigningKey.from_string(
2021-10-17 18:33:29 +01:00
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
2020-12-31 18:50:16 +01:00
)
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
2021-10-22 00:41:30 +01:00
from .crud import get_standalone_payment
2021-10-22 00:41:30 +01:00
return await get_standalone_payment(payment_hash)
class WalletType(Enum):
admin = 0
invoice = 1
invalid = 2
# backwards compatibility
def __eq__(self, other):
return self.value == other
@dataclass
class WalletTypeInfo:
wallet_type: WalletType
wallet: Wallet
class User(BaseModel):
id: str
email: Optional[str] = None
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
2022-01-31 16:29:42 +00:00
admin: bool = False
super_user: bool = False
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@classmethod
def is_extension_for_user(cls, ext: str, user: str) -> bool:
if ext not in settings.lnbits_admin_extensions:
return True
if user == settings.super_user:
return True
if user in settings.lnbits_admin_users:
return True
return False
class Payment(FromRowModel):
checking_id: str
pending: bool
amount: int
fee: int
2021-11-04 12:57:28 +00:00
memo: Optional[str]
time: int
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[float]
extra: Dict = {}
wallet_id: str
2021-08-29 19:38:42 +02:00
webhook: Optional[str]
webhook_status: Optional[int]
@classmethod
def from_row(cls, row: Row):
return cls(
checking_id=row["checking_id"],
payment_hash=row["hash"] or "0" * 64,
bolt11=row["bolt11"] or "",
preimage=row["preimage"] or "0" * 64,
extra=json.loads(row["extra"] or "{}"),
pending=row["pending"],
amount=row["amount"],
fee=row["fee"],
memo=row["memo"],
time=row["time"],
2022-12-02 18:51:18 +01:00
expiry=row["expiry"],
wallet_id=row["wallet"],
webhook=row["webhook"],
webhook_status=row["webhook_status"],
)
@property
def tag(self) -> Optional[str]:
2022-07-19 18:51:35 +02:00
if self.extra is None:
return ""
return self.extra.get("tag")
@property
def msat(self) -> int:
return self.amount
@property
def sat(self) -> int:
2020-04-26 13:28:19 +02:00
return self.amount // 1000
@property
def is_in(self) -> bool:
return self.amount > 0
@property
def is_out(self) -> bool:
return self.amount < 0
2022-12-02 18:51:18 +01:00
@property
def is_expired(self) -> bool:
return self.expiry < time.time() if self.expiry else False
2022-12-02 18:51:18 +01:00
@property
def is_uncheckable(self) -> bool:
return self.checking_id.startswith("internal_")
async def update_status(
self,
status: PaymentStatus,
conn: Optional[Connection] = None,
) -> None:
from .crud import update_payment_details
await update_payment_details(
checking_id=self.checking_id,
pending=status.pending,
fee=status.fee_msat,
preimage=status.preimage,
conn=conn,
)
async def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status
self.pending = pending
await update_payment_status(self.checking_id, pending)
async def check_status(
self,
conn: Optional[Connection] = None,
) -> PaymentStatus:
2020-09-27 23:12:55 -03:00
if self.is_uncheckable:
return PaymentStatus(None)
2020-09-27 23:12:55 -03:00
logger.debug(
f"Checking {'outgoing' if self.is_out else 'incoming'} "
f"pending payment {self.checking_id}"
)
2022-10-05 09:46:59 +02:00
WALLET = get_wallet_class()
2020-09-27 23:12:55 -03:00
if self.is_out:
status = await WALLET.get_payment_status(self.checking_id)
2020-09-27 23:12:55 -03:00
else:
status = await WALLET.get_invoice_status(self.checking_id)
2020-09-27 23:12:55 -03:00
logger.debug(f"Status: {status}")
2022-12-06 13:21:34 +01:00
if self.is_in and status.pending and self.is_expired and self.expiry:
2022-12-02 18:51:18 +01:00
expiration_date = datetime.datetime.fromtimestamp(self.expiry)
logger.debug(
f"Deleting expired incoming pending payment {self.checking_id}: "
f"expired {expiration_date}"
2022-12-02 18:51:18 +01:00
)
await self.delete(conn)
# wait at least 15 minutes before deleting failed outgoing payments
elif self.is_out and status.failed and self.time + 900 < int(time.time()):
logger.warning(
f"Deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete(conn)
elif not status.pending:
logger.info(
f"Marking '{'in' if self.is_in else 'out'}' "
f"{self.checking_id} as not pending anymore: {status}"
)
await self.update_status(status, conn=conn)
return status
2020-09-27 23:12:55 -03:00
async def delete(self, conn: Optional[Connection] = None) -> None:
from .crud import delete_wallet_payment
2020-03-05 20:29:27 +01:00
await delete_wallet_payment(self.checking_id, self.wallet_id, conn=conn)
2021-04-17 18:27:15 -03:00
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount"]
checking_id: str
amount: int
fee: int
memo: Optional[str]
time: datetime.datetime
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[datetime.datetime]
extra: Dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
class PaymentHistoryPoint(BaseModel):
date: datetime.datetime
income: int
spending: int
balance: int
2021-08-20 21:31:01 +01:00
class BalanceCheck(BaseModel):
2021-04-17 18:27:15 -03:00
wallet: str
service: str
url: str
@classmethod
def from_row(cls, row: Row):
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
[FEAT] add extension functionality to lnbits-cli (#1963) * [FEAT] add extension functionality to lnbits-cli WIP draft cli commands for vlad :) * add extension list command * [feat] lnbits-cli add install, uninstall and upgrade * feat: load settings from DB * refactor: simplify settings loading * feat: show current version if installed * feat: add mor emessages * feat: basic DB install * feat: add extension * feat: do not install if the server is up * feat: add logic for uninstall * refactor: prepare for upgrade * feat: check extension before upgrade * refactor: stuff * fix: have a default value * feat: use the API logic * feat: use pi methods for un-install * refactor: extract _select_release * feat: add flags * feat: check if extension already up to date * refactor: use `_run_async` * feat: install all extensions * feat: install online * fix: api install * fix: API upgrade & install * feat: add API uninstall * failed typo * typo running * url duplication * [fix] provide short-options too (same as upgrade command) Co-authored-by: Pavol Rusnak <pavol@rusnak.io> * make black * fix: fail if .superuser file not found; add `--admin-user` option * fix: ambiguous use of `logger.debug` - register_new_ext_routes must not be None - `logger.debug` was used because it allowed any arguments, but that was a bad idea - now an explicit empty `_do_nothing(*_)` function is used * fix: load settings * doc: updated `--source-repo` * chore: rename `upgrade` to `update` * refactor: use `@annotation` for making commands async * fix: code checks --------- Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-10-25 14:03:00 +02:00
def _do_nothing(*_):
pass
class CoreAppExtra:
[FEAT] add extension functionality to lnbits-cli (#1963) * [FEAT] add extension functionality to lnbits-cli WIP draft cli commands for vlad :) * add extension list command * [feat] lnbits-cli add install, uninstall and upgrade * feat: load settings from DB * refactor: simplify settings loading * feat: show current version if installed * feat: add mor emessages * feat: basic DB install * feat: add extension * feat: do not install if the server is up * feat: add logic for uninstall * refactor: prepare for upgrade * feat: check extension before upgrade * refactor: stuff * fix: have a default value * feat: use the API logic * feat: use pi methods for un-install * refactor: extract _select_release * feat: add flags * feat: check if extension already up to date * refactor: use `_run_async` * feat: install all extensions * feat: install online * fix: api install * fix: API upgrade & install * feat: add API uninstall * failed typo * typo running * url duplication * [fix] provide short-options too (same as upgrade command) Co-authored-by: Pavol Rusnak <pavol@rusnak.io> * make black * fix: fail if .superuser file not found; add `--admin-user` option * fix: ambiguous use of `logger.debug` - register_new_ext_routes must not be None - `logger.debug` was used because it allowed any arguments, but that was a bad idea - now an explicit empty `_do_nothing(*_)` function is used * fix: load settings * doc: updated `--source-repo` * chore: rename `upgrade` to `update` * refactor: use `@annotation` for making commands async * fix: code checks --------- Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-10-25 14:03:00 +02:00
register_new_ext_routes: Callable = _do_nothing
register_new_ratelimiter: Callable
2023-01-25 09:56:05 +02:00
2023-01-12 15:16:37 +00:00
class TinyURL(BaseModel):
id: str
url: str
endless: bool
wallet: str
time: float
2023-01-12 15:16:37 +00:00
@classmethod
2023-01-23 19:57:49 +00:00
def from_row(cls, row: Row):
2023-01-24 13:30:15 +00:00
return cls(**dict(row))
class ConversionData(BaseModel):
from_: str = "sat"
amount: float
to: str = "usd"
class Callback(BaseModel):
callback: str
class DecodePayment(BaseModel):
data: str
class CreateLnurl(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
class CreateInvoice(BaseModel):
unit: str = "sat"
internal: bool = False
out: bool = True
amount: float = Query(None, ge=0)
memo: Optional[str] = None
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
expiry: Optional[int] = None
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
bolt11: Optional[str] = None
2023-08-24 11:52:12 +02:00
class CreateTopup(BaseModel):
id: str
amount: int
class CreateLnurlAuth(BaseModel):
callback: str
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
class CreateWallet(BaseModel):
name: Optional[str] = None
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
class CreateWebPushSubscription(BaseModel):
subscription: str
class WebPushSubscription(BaseModel):
endpoint: str
user: str
data: str
host: str
timestamp: str