2022-12-02 18:51:52 +01:00
|
|
|
import datetime
|
2020-11-10 04:25:46 +01:00
|
|
|
import hashlib
|
2022-07-16 14:23:03 +02:00
|
|
|
import hmac
|
|
|
|
import json
|
2022-12-02 18:51:18 +01:00
|
|
|
import time
|
2020-09-01 03:12:46 +02:00
|
|
|
from sqlite3 import Row
|
2022-12-27 14:50:42 +01:00
|
|
|
from typing import Dict, List, Optional
|
2022-07-07 14:30:16 +02:00
|
|
|
|
2023-01-09 11:14:44 +01:00
|
|
|
from ecdsa import SECP256k1, SigningKey
|
2022-12-08 14:41:52 +01:00
|
|
|
from fastapi import Query
|
2023-01-09 11:14:44 +01:00
|
|
|
from lnurl import encode as lnurl_encode
|
2022-07-07 14:30:16 +02:00
|
|
|
from loguru import logger
|
2022-12-27 14:50:42 +01:00
|
|
|
from pydantic import BaseModel
|
2022-07-07 14:30:16 +02:00
|
|
|
|
2022-09-12 19:57:23 +02:00
|
|
|
from lnbits.db import Connection
|
2022-07-16 14:23:03 +02:00
|
|
|
from lnbits.helpers import url_for
|
2022-10-05 09:46:59 +02:00
|
|
|
from lnbits.settings import get_wallet_class
|
2022-08-30 13:28:58 +02:00
|
|
|
from lnbits.wallets.base import PaymentStatus
|
2020-09-28 04:12:55 +02:00
|
|
|
|
2020-03-04 23:11:15 +01:00
|
|
|
|
2021-08-20 22:31:01 +02:00
|
|
|
class Wallet(BaseModel):
|
2020-03-04 23:11:15 +01:00
|
|
|
id: str
|
|
|
|
name: str
|
|
|
|
user: str
|
|
|
|
adminkey: str
|
|
|
|
inkey: str
|
2020-03-07 22:27:00 +01:00
|
|
|
balance_msat: int
|
2020-03-04 23:11:15 +01:00
|
|
|
|
2020-03-07 22:27:00 +01:00
|
|
|
@property
|
|
|
|
def balance(self) -> int:
|
2020-08-31 21:19:27 +02:00
|
|
|
return self.balance_msat // 1000
|
2020-03-07 22:27:00 +01:00
|
|
|
|
2021-04-17 23:27:15 +02: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 19:33:29 +02:00
|
|
|
|
|
|
|
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
2021-04-22 04:27:21 +02:00
|
|
|
try:
|
|
|
|
return lnurl_encode(url)
|
|
|
|
except:
|
|
|
|
return ""
|
2021-04-17 23:27:15 +02:00
|
|
|
|
2020-11-12 02:37:55 +01:00
|
|
|
def lnurlauth_key(self, domain: str) -> SigningKey:
|
2022-12-29 16:50:05 +01:00
|
|
|
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
|
|
|
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
2020-11-12 02:37:55 +01:00
|
|
|
|
2020-12-31 18:50:16 +01:00
|
|
|
return SigningKey.from_string(
|
2021-10-17 19:33:29 +02:00
|
|
|
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
2020-12-31 18:50:16 +01:00
|
|
|
)
|
2020-11-10 04:25:46 +01:00
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
2021-10-22 01:41:30 +02:00
|
|
|
from .crud import get_standalone_payment
|
2020-03-04 23:11:15 +01:00
|
|
|
|
2021-10-22 01:41:30 +02:00
|
|
|
return await get_standalone_payment(payment_hash)
|
2020-03-04 23:11:15 +01:00
|
|
|
|
|
|
|
|
2021-08-24 21:23:18 +02:00
|
|
|
class User(BaseModel):
|
|
|
|
id: str
|
|
|
|
email: Optional[str] = None
|
|
|
|
extensions: List[str] = []
|
|
|
|
wallets: List[Wallet] = []
|
|
|
|
password: Optional[str] = None
|
2022-01-31 17:29:42 +01:00
|
|
|
admin: bool = False
|
2022-12-07 11:00:48 +01:00
|
|
|
super_user: bool = False
|
2021-08-24 21:23:18 +02:00
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
2021-08-20 22:31:01 +02:00
|
|
|
class Payment(BaseModel):
|
2020-03-31 19:05:25 +02:00
|
|
|
checking_id: str
|
2020-03-04 23:11:15 +01:00
|
|
|
pending: bool
|
|
|
|
amount: int
|
|
|
|
fee: int
|
2021-11-04 13:57:28 +01:00
|
|
|
memo: Optional[str]
|
2020-03-04 23:11:15 +01:00
|
|
|
time: int
|
2020-09-01 03:12:46 +02:00
|
|
|
bolt11: str
|
|
|
|
preimage: str
|
|
|
|
payment_hash: str
|
2022-12-06 13:08:36 +01:00
|
|
|
expiry: Optional[float]
|
2023-01-04 10:52:19 +01:00
|
|
|
extra: Dict = {}
|
2020-10-06 06:50:55 +02:00
|
|
|
wallet_id: str
|
2021-08-29 19:38:42 +02:00
|
|
|
webhook: Optional[str]
|
|
|
|
webhook_status: Optional[int]
|
2020-09-01 03:12:46 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_row(cls, row: Row):
|
|
|
|
return cls(
|
|
|
|
checking_id=row["checking_id"],
|
2020-09-12 03:14:25 +02:00
|
|
|
payment_hash=row["hash"] or "0" * 64,
|
|
|
|
bolt11=row["bolt11"] or "",
|
|
|
|
preimage=row["preimage"] or "0" * 64,
|
2020-09-01 03:12:46 +02:00
|
|
|
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"],
|
2020-10-06 06:50:55 +02:00
|
|
|
wallet_id=row["wallet"],
|
2020-12-24 13:38:35 +01:00
|
|
|
webhook=row["webhook"],
|
|
|
|
webhook_status=row["webhook_status"],
|
2020-09-01 03:12:46 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tag(self) -> Optional[str]:
|
2022-07-19 18:51:35 +02:00
|
|
|
if self.extra is None:
|
|
|
|
return ""
|
2020-09-01 03:12:46 +02:00
|
|
|
return self.extra.get("tag")
|
2020-03-04 23:11:15 +01:00
|
|
|
|
|
|
|
@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
|
2020-03-04 23:11:15 +01:00
|
|
|
|
|
|
|
@property
|
2020-03-07 22:27:00 +01:00
|
|
|
def is_in(self) -> bool:
|
|
|
|
return self.amount > 0
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_out(self) -> bool:
|
|
|
|
return self.amount < 0
|
2020-03-04 23:11:15 +01:00
|
|
|
|
2022-12-02 18:51:18 +01:00
|
|
|
@property
|
|
|
|
def is_expired(self) -> bool:
|
2022-12-06 13:08:36 +01:00
|
|
|
return self.expiry < time.time() if self.expiry else False
|
2022-12-02 18:51:18 +01:00
|
|
|
|
2020-09-02 05:32:52 +02:00
|
|
|
@property
|
|
|
|
def is_uncheckable(self) -> bool:
|
2022-08-30 13:28:58 +02:00
|
|
|
return self.checking_id.startswith("internal_")
|
|
|
|
|
2022-09-12 19:57:23 +02:00
|
|
|
async def update_status(
|
|
|
|
self,
|
|
|
|
status: PaymentStatus,
|
|
|
|
conn: Optional[Connection] = None,
|
|
|
|
) -> None:
|
2022-08-30 13:28:58 +02:00
|
|
|
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,
|
2022-09-12 19:57:23 +02:00
|
|
|
conn=conn,
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2020-09-02 05:32:52 +02:00
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
async def set_pending(self, pending: bool) -> None:
|
2020-03-07 22:27:00 +01:00
|
|
|
from .crud import update_payment_status
|
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
await update_payment_status(self.checking_id, pending)
|
2020-03-07 22:27:00 +01:00
|
|
|
|
2022-09-12 19:57:23 +02:00
|
|
|
async def check_status(
|
|
|
|
self,
|
|
|
|
conn: Optional[Connection] = None,
|
|
|
|
) -> PaymentStatus:
|
2020-09-28 04:12:55 +02:00
|
|
|
if self.is_uncheckable:
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentStatus(None)
|
2020-09-28 04:12:55 +02:00
|
|
|
|
2022-08-09 11:49:39 +02:00
|
|
|
logger.debug(
|
|
|
|
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
|
|
|
|
)
|
|
|
|
|
2022-10-05 09:46:59 +02:00
|
|
|
WALLET = get_wallet_class()
|
2020-09-28 04:12:55 +02:00
|
|
|
if self.is_out:
|
2021-03-28 05:11:41 +02:00
|
|
|
status = await WALLET.get_payment_status(self.checking_id)
|
2020-09-28 04:12:55 +02:00
|
|
|
else:
|
2021-03-28 05:11:41 +02:00
|
|
|
status = await WALLET.get_invoice_status(self.checking_id)
|
2020-09-28 04:12:55 +02:00
|
|
|
|
2022-08-09 11:49:39 +02: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(
|
2022-12-06 13:08:36 +01:00
|
|
|
f"Deleting expired incoming pending payment {self.checking_id}: expired {expiration_date}"
|
2022-12-02 18:51:18 +01:00
|
|
|
)
|
|
|
|
await self.delete(conn)
|
2022-12-06 13:21:34 +01:00
|
|
|
elif self.is_out and status.failed:
|
2022-08-30 13:28:58 +02:00
|
|
|
logger.warning(
|
2022-08-09 11:49:39 +02:00
|
|
|
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
2022-07-07 14:30:16 +02:00
|
|
|
)
|
2022-09-15 13:48:59 +02:00
|
|
|
await self.delete(conn)
|
2021-03-28 05:11:41 +02:00
|
|
|
elif not status.pending:
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.info(
|
2022-08-09 11:49:39 +02:00
|
|
|
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
2021-04-01 01:24:46 +02:00
|
|
|
)
|
2022-09-12 19:57:23 +02:00
|
|
|
await self.update_status(status, conn=conn)
|
2022-08-30 13:28:58 +02:00
|
|
|
return status
|
2020-09-28 04:12:55 +02:00
|
|
|
|
2022-09-15 13:48:59 +02:00
|
|
|
async def delete(self, conn: Optional[Connection] = None) -> None:
|
2020-03-07 22:27:00 +01:00
|
|
|
from .crud import delete_payment
|
2020-03-05 20:29:27 +01:00
|
|
|
|
2022-09-15 13:48:59 +02:00
|
|
|
await delete_payment(self.checking_id, conn=conn)
|
2021-04-17 23:27:15 +02:00
|
|
|
|
|
|
|
|
2021-08-20 22:31:01 +02:00
|
|
|
class BalanceCheck(BaseModel):
|
2021-04-17 23:27:15 +02: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"])
|
2023-01-12 16:16:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
class TinyURL(BaseModel):
|
|
|
|
id: str
|
|
|
|
url: str
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_row(cls, row: Row) -> "TinyURL":
|
|
|
|
return cls(**dict(row))
|