diff --git a/__init__.py b/__init__.py index f0adf33..e7b2717 100644 --- a/__init__.py +++ b/__init__.py @@ -4,14 +4,11 @@ from fastapi import APIRouter from loguru import logger from lnbits.tasks import create_permanent_unique_task - from .crud import db from .tasks import handle_execution_queue, handle_nwc from .views import nwcprovider_router from .views_api import nwcprovider_api_router -scheduled_tasks: list[asyncio.Task] = [] - nwcprovider_ext: APIRouter = APIRouter( prefix="/nwcprovider", tags=["NWC Service Provider"] ) @@ -25,6 +22,7 @@ nwcprovider_static_files = [ } ] +scheduled_tasks: list[asyncio.Task] = [] def nwcprovider_stop(): for task in scheduled_tasks: @@ -42,5 +40,10 @@ def nwcprovider_start(): ) scheduled_tasks.append(task) - -__all__ = ["db"] +__all__ = [ + "db", + "nwcprovider_ext", + "nwcprovider_static_files", + "nwcprovider_start", + "nwcprovider_stop", +] diff --git a/config.json b/config.json index 66d391d..92b27df 100644 --- a/config.json +++ b/config.json @@ -2,13 +2,31 @@ "name": "NWC Service Provider", "short_description": "A NWC service provider for LNbits", "tile": "/nwcprovider/static/image/nwcprovider.png", - "min_lnbits_version": "0.12.5", + "min_lnbits_version": "1.0.0", "contributors": [ { "name": "Riccardo Balbo", "uri": "https://github.com/riccardobl", + "role": "Lead Dev" + }, + { + "name": "Ben Arc", + "uri": "https://github.com/arcbtc", "role": "Dev" } ], + "images": [ + { + "uri": "https://raw.githubusercontent.com/riccardobl/nwcprovider/main/static/image/1.png" + }, + { + "uri": "https://raw.githubusercontent.com/riccardobl/nwcprovider/main/static/image/2.png" + }, + { + "uri": "https://raw.githubusercontent.com/riccardobl/nwcprovider/main/static/image/3.png" + } + ], + "description_md": "https://raw.githubusercontent.com/riccardobl/nwcprovider/main/description.md", + "terms_and_conditions_md": "https://raw.githubusercontent.com/riccardobl/nwcprovider/main/toc.md", "license": "MIT" } diff --git a/crud.py b/crud.py index 806cbf2..c68aa0b 100644 --- a/crud.py +++ b/crud.py @@ -4,113 +4,70 @@ from typing import List, Optional from lnbits.db import Database from .execution_queue import enqueue -from .models import NWCBudget, NWCKey, NWCNewBudget +from .models import ( + NWCBudget, + NWCKey, + CreateNWCKey, + GetNWCKey, + GetWalletNWC, + GetBudgetsNWC, + TrackedSpendNWC, + DeleteNWC +) db = Database("ext_nwcprovider") - -async def create_nwc( - pubkey: str, - wallet_id: str, - description: str, - expires_at: int, - permissions: List[str], - budgets: Optional[List[NWCNewBudget]] = None, -) -> NWCKey: - # Check if the key already exists - if await get_nwc(pubkey, None, True): - raise Exception("Public key already used") - # If not, create it - now = int(time.time()) - await db.execute( - """ - INSERT INTO nwcprovider.keys ( - pubkey, - wallet, - description, - permissions, - created_at, - expires_at, - last_used - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - ( - pubkey, - wallet_id, - description, - " ".join(permissions), - now, - int(expires_at) if expires_at else 0, - now, - ), +async def create_nwc(data: CreateNWCKey) -> NWCKey: + nwckey_entry = NWCKey( + pubkey=data.pubkey, + wallet=data.wallet_id, + description=data.description, + expires_at=int(data.expires_at) if data.expires_at else 0, + permissions=" ".join(data.permissions), + created_at=int(time.time()), + last_used=int(time.time()), ) - # Add budgets - if budgets: - for budget in budgets: - await db.execute( - """ - INSERT INTO nwcprovider.budgets ( - pubkey, - budget_msats, - refresh_window, - created_at - ) - VALUES (?, ?, ?, ?) - """, - (pubkey, budget.budget_msats, budget.refresh_window, budget.created_at), + await db.insert("nwcprovider.keys", nwckey_entry) + if data.budgets: + for budget in data.budgets: + budget_entry = NWCKey( + pubkey=data.pubkey, + budget_msats=budget.budget_msats, + refresh_window=budget.refresh_window, + created_at=budget.created_at ) - # Return the created key - return NWCKey( - pubkey=pubkey, - wallet=wallet_id, - description=description, - expires_at=expires_at, - permissions=" ".join(permissions), - created_at=now, - last_used=now, - ) + await db.insert("nwcprovider.budgets", budget_entry) + return NWCKey(**data.dict()) - -async def delete_nwc(pubkey: str, wallet_id: str): - nwc = await get_nwc(pubkey, wallet_id) - if not nwc: - raise Exception("Public key does not exist") +async def delete_nwc(data:DeleteNWC) -> None: await db.execute( - """ - DELETE FROM nwcprovider.keys WHERE pubkey = ? AND wallet = ? - """, - (pubkey, wallet_id), + "DELETE FROM nwcprovider.keys WHERE pubkey = :pubkey AND wallet = :wallet", {"pubkey": data.pubkey, "wallet": data.wallet_id} ) - -async def get_wallet_nwcs( - wallet_id: str, include_expired: Optional[bool] = False -) -> List[NWCKey]: - rows = await db.fetchall( +async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]: + return await db.fetchall( """ - SELECT * FROM nwcprovider.keys - WHERE wallet = ? AND (expires_at = 0 OR expires_at > ?) - """, - (wallet_id, int(time.time()) if not include_expired else -1), + SELECT * FROM nwcprovider.keys + WHERE wallet = :wallet AND (expires_at = 0 OR expires_at > :expires) + """, + {"wallet": data.wallet_id, "expires": int(time.time()) if not data.include_expired else -1}, + model=NWCKey, ) - return [NWCKey(**row) for row in rows] - async def get_nwc( - pubkey: str, - wallet_id: Optional[str] = None, - include_expired: Optional[bool] = False, - refresh_last_used: Optional[bool] = False, + data: GetNWCKey ) -> Optional[NWCKey]: # expires_at = 0 means it never expires - if wallet_id: + if data.wallet_id: row = await db.fetchone( """ - SELECT * FROM nwcprovider.keys - WHERE pubkey = ? AND wallet = ? AND (expires_at = 0 OR expires_at > ?) + SELECT * FROM nwcprovider.keys + WHERE pubkey = :pubkey AND wallet = :wallet + AND (expires_at = 0 OR expires_at > :expires) """, - (pubkey, wallet_id, int(time.time()) if not include_expired else -1), + {"pubkey": data.pubkey, "wallet": data.wallet_id, + "expires": int(time.time()) if not data.include_expired else -1}, + NWCKey, ) else: row = await db.fetchone( @@ -118,44 +75,52 @@ async def get_nwc( SELECT * FROM nwcprovider.keys WHERE pubkey = ? AND (expires_at = 0 OR expires_at > ?) """, - (pubkey, int(time.time()) if not include_expired else -1), + (data.pubkey, int(time.time()) if not data.include_expired else -1), + ) + row = await db.fetchone( + """ + SELECT * FROM nwcprovider.keys + WHERE pubkey = :pubkey AND (expires_at = 0 OR expires_at > :expires) + """, + {"pubkey": data.pubkey, "expires": int(time.time()) if not data.include_expired else -1}, + NWCKey, ) if not row: return None - if refresh_last_used: + if data.refresh_last_used: await db.execute( """ - UPDATE nwcprovider.keys SET last_used = ? WHERE pubkey = ? + UPDATE nwcprovider.keys SET last_used = :last_used WHERE pubkey = :pubkey """, - (int(time.time()), pubkey), + {"last_used":int(time.time()), "pubkey":data.pubkey}, ) return NWCKey(**row) -async def get_budgets_nwc(pubkey, calculate_spent=False): +async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]: rows = await db.fetchall( - "SELECT * FROM nwcprovider.budgets WHERE pubkey = ?", (pubkey) + "SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey", {"pubkey":data.pubkey} ) budgets = [NWCBudget(**row) for row in rows] - if calculate_spent: + if data.calculate_spent: for budget in budgets: last_cycle, next_cycle = budget.get_timestamp_range() tot_spent_in_range_msats = await db.fetchone( """ SELECT SUM(amount_msats) FROM nwcprovider.spent - WHERE pubkey = ? AND created_at >= ? AND created_at < ? + WHERE pubkey = :pubkey AND created_at >= :last_cycle AND created_at < next_cycle """, - (pubkey, last_cycle, next_cycle), + {"pubkey":data.pubkey, "last_cycle":last_cycle, "next_cycle":next_cycle}, ) tot_spent_in_range_msats = tot_spent_in_range_msats[0] or 0 budget.used_budget_msats = tot_spent_in_range_msats return budgets -async def tracked_spend_nwc(pubkey: str, amount_msats: int, action): +async def tracked_spend_nwc(data:TrackedSpendNWC, action): async def r(): created_at = int(time.time()) - budgets = await get_budgets_nwc(pubkey) + budgets = await get_budgets_nwc(data.pubkey) in_budget = True for budget in budgets: last_cycle, next_cycle = budget.get_timestamp_range() @@ -164,14 +129,14 @@ async def tracked_spend_nwc(pubkey: str, amount_msats: int, action): await db.fetchone( """ SELECT SUM(amount_msats) FROM nwcprovider.spent - WHERE pubkey = ? AND created_at >= ? AND created_at < ? + WHERE pubkey = :pubkey AND created_at >= :last_cycle AND created_at < :next_cycle """, - (pubkey, last_cycle, next_cycle), + {"pubkey":data.pubkey, "last_cycle":last_cycle, "next_cycle":next_cycle}, ) )[0] or 0 ) - if tot_spent_in_range_msats + amount_msats > budget.budget_msats: + if tot_spent_in_range_msats + data.amount_msats > budget.budget_msats: in_budget = False break if not in_budget: @@ -180,9 +145,9 @@ async def tracked_spend_nwc(pubkey: str, amount_msats: int, action): await db.execute( """ INSERT INTO nwcprovider.spent (pubkey, amount_msats, created_at) - VALUES (?, ?, ?) + VALUES (:pubkey, :amount_msats, :created_at) """, - (pubkey, amount_msats, created_at), + {"pubkey":data.pubkey, "amount_msats":data.amount_msats, "created_at":created_at}, ) return True, out @@ -190,29 +155,27 @@ async def tracked_spend_nwc(pubkey: str, amount_msats: int, action): async def get_config_nwc(key: str): - row = await db.fetchone("SELECT * FROM nwcprovider.config WHERE key = ?", (key,)) + row = await db.fetchone("SELECT * FROM nwcprovider.config WHERE key = :key", {"key":key}) if not row: return None return row["value"] - -async def get_all_config_nwc(): - rows = await db.fetchall("SELECT * FROM nwcprovider.config") - return {row["key"]: row["value"] for row in rows} - - async def set_config_nwc(key: str, value: str): await db.execute( """ DELETE FROM nwcprovider.config - WHERE key = ? + WHERE key = :key """, - (key,), + {"key":key}, ) await db.execute( """ INSERT INTO nwcprovider.config (key, value) - VALUES (?, ?) + VALUES (:key, :value) """, - (key, value), + {"key":key, "value":value}, ) + +async def get_all_config_nwc(): + rows = await db.fetchall("SELECT * FROM nwcprovider.config") + return {row["key"]: row["value"] for row in rows} \ No newline at end of file diff --git a/description.md b/description.md new file mode 100644 index 0000000..e76978b --- /dev/null +++ b/description.md @@ -0,0 +1,3 @@ +NWC Service Provider Extension for https://github.com/lnbits/lnbits + +Easily connect your LNbits wallets via https://nwc.dev/ \ No newline at end of file diff --git a/models.py b/models.py index 83f0b2a..0460427 100644 --- a/models.py +++ b/models.py @@ -2,10 +2,10 @@ import time from sqlite3 import Row -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from pydantic import BaseModel - +from .nwcp import NWCServiceProvider class NWCKey(BaseModel): pubkey: str @@ -26,6 +26,36 @@ class NWCKey(BaseModel): def from_row(cls, row: Dict[str, Any]) -> "NWCKey": return cls(**row) +class GetNWCKey(BaseModel): + pubkey: str + wallet_id: Optional[str] = None + include_expired: Optional[bool] = False + refresh_last_used: Optional[bool] = False + +class GetNWCs(BaseModel): + include_expired: bool = False + calculate_spent_budget: bool = False + +class GetWalletNWC(BaseModel): + wallet_id: Optional[str] = None + include_expired: Optional[bool] = False + +class GetBudgetsNWC(BaseModel): + pubkey: str + calculate_spent: Optional[bool] = False + +class TrackedSpendNWC(BaseModel): + pubkey: str + amount_msats: int + +class DeleteNWC(BaseModel): + pubkey: str + wallet_id: Optional[str] = None + +class OnInvoicePaid(BaseModel): + sp: NWCServiceProvider + pubkey: str + payload: Dict class NWCBudget(BaseModel): id: int @@ -75,7 +105,18 @@ class NWCRegistrationRequest(BaseModel): expires_at: int budgets: List[NWCNewBudget] +class RegisterNWC(BaseModel): + pubkey: str + registration_data: NWCRegistrationRequest class NWCGetResponse(BaseModel): data: NWCKey budgets: List[NWCBudget] + +class CreateNWCKey(BaseModel): + pubkey: str + wallet_id: str + description: str + expires_at: int + permissions: List[str] + budgets: Optional[List[NWCNewBudget]] = None \ No newline at end of file diff --git a/tasks.py b/tasks.py index 356bb43..1f03e5f 100644 --- a/tasks.py +++ b/tasks.py @@ -19,12 +19,12 @@ from loguru import logger from .crud import get_config_nwc, get_nwc, tracked_spend_nwc from .execution_queue import execution_queue -from .models import NWCKey +from .models import NWCKey, OnInvoicePaid from .nwcp import NWCServiceProvider from .permission import nwc_permissions -async def _check(nwc: Optional[NWCKey], method: str, payload: Dict) -> Optional[Dict]: +async def _check(nwc: Optional[NWCKey], method: str) -> Optional[Dict]: # check if not nwc: return { @@ -106,15 +106,15 @@ async def _process_invoice( async def _on_pay_invoice( - sp: NWCServiceProvider, pubkey: str, payload: Dict -) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "pay_invoice", payload) + data:OnInvoicePaid + ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "pay_invoice", data.payload) if error: return [(None, error, [])] if not nwc: raise Exception("Pubkey has no associated wallet") - params = payload.get("params", {}) + params = data.payload.get("params", {}) invoice = params.get("invoice", None) # Ensures invoice is provided if not invoice: @@ -122,7 +122,7 @@ async def _on_pay_invoice( invoice_data = bolt11_decode(invoice) amount_msats = int(invoice_data.amount_msat or 0) res = await _process_invoice( - nwc.wallet, pubkey, invoice, amount_msats, invoice_data.description + nwc.wallet, data.pubkey, invoice, amount_msats, invoice_data.description ) error = res.get("error") if error: @@ -136,15 +136,15 @@ async def _on_pay_invoice( async def _on_multi_pay_invoice( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "multi_pay_invoice", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "multi_pay_invoice", data.payload) if error: return [(None, error, [])] if not nwc: raise Exception("Pubkey has no associated wallet") - params = payload.get("params", {}) + params = data.payload.get("params", {}) invoices = params.get("invoices", []) results: List[Tuple[Optional[Dict], Optional[Dict], List]] = [] @@ -182,15 +182,15 @@ async def _on_multi_pay_invoice( async def _on_make_invoice( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "make_invoice", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "make_invoice", data.payload) if error: return [(None, error, [])] if not nwc: raise Exception("Pubkey has no associated wallet") - params = payload.get("params", {}) + params = data.payload.get("params", {}) amount_msats = params.get("amount", None) # Ensures amount is provided if not amount_msats: @@ -234,15 +234,15 @@ async def _on_make_invoice( async def _on_lookup_invoice( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "lookup_invoice", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "lookup_invoice", data.payload) if error: return [(None, error, [])] if not nwc: raise Exception("Pubkey has no associated wallet") - params = payload.get("params", {}) + params = data.payload.get("params", {}) payment_hash = params.get("payment_hash", None) invoice = params.get("invoice", None) # Ensure payment_hash or invoice are provided @@ -281,20 +281,20 @@ async def _on_lookup_invoice( async def _on_list_transactions( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "list_transactions", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "list_transactions", data.payload) if error: return [(None, error, [])] if not nwc: raise Exception("Pubkey has no associated wallet") - tfrom = payload.get("from", 0) - tto = payload.get("to", int(time.time())) - limit = payload.get("limit", 10) - offset = payload.get("offset", 0) - unpaid = payload.get("unpaid", False) - tx_type = payload.get("type", None) + tfrom = data.payload.get("from", 0) + tto = data.payload.get("to", int(time.time())) + limit = data.payload.get("limit", 10) + offset = data.payload.get("offset", 0) + unpaid = data.payload.get("unpaid", False) + tx_type = data.payload.get("type", None) values = [] filters: Filters = Filters() filters.where(["time <= ?"]) @@ -337,10 +337,10 @@ async def _on_list_transactions( async def _on_get_balance( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "get_balance", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "get_balance", data.payload) if error: return [(None, error, [])] if not nwc: @@ -355,10 +355,10 @@ async def _on_get_balance( async def _on_get_info( - sp: NWCServiceProvider, pubkey: str, payload: Dict + data:OnInvoicePaid ) -> List[Tuple[Optional[Dict], Optional[Dict], List]]: - nwc = await get_nwc(pubkey, None, False, True) - error = await _check(nwc, "get_info", payload) + nwc = await get_nwc(data.pubkey, None, False, True) + error = await _check(nwc, "get_info", data.payload) if error: return [(None, error, [])] if not nwc: diff --git a/templates/nwcprovider/admin.html b/templates/nwcprovider/admin.html index dbd87ba..3a64166 100644 --- a/templates/nwcprovider/admin.html +++ b/templates/nwcprovider/admin.html @@ -38,8 +38,8 @@ {% endblock %} {% block scripts %} {{ window_vars(user) }}