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) }}