Merge branch 'v1_update'

This commit is contained in:
Arc 2024-11-19 22:31:49 +00:00
commit ef53a01bb7
12 changed files with 1185 additions and 1028 deletions

View file

@ -33,7 +33,7 @@ checkeditorconfig:
test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest tests/unit/*.py -s
poetry run pytest
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"

View file

@ -1,9 +1,9 @@
import asyncio
from fastapi import APIRouter
from lnbits.tasks import create_permanent_unique_task
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
@ -24,6 +24,7 @@ nwcprovider_static_files = [
scheduled_tasks: list[asyncio.Task] = []
def nwcprovider_stop():
for task in scheduled_tasks:
try:
@ -40,6 +41,7 @@ def nwcprovider_start():
)
scheduled_tasks.append(task)
__all__ = [
"db",
"nwcprovider_ext",

84
crud.py
View file

@ -5,18 +5,19 @@ from lnbits.db import Database
from .execution_queue import enqueue
from .models import (
NWCBudget,
NWCKey,
CreateNWCKey,
DeleteNWC,
GetBudgetsNWC,
GetNWCKey,
GetWalletNWC,
GetBudgetsNWC,
NWCBudget,
NWCKey,
TrackedSpendNWC,
DeleteNWC
)
db = Database("ext_nwcprovider")
async def create_nwc(data: CreateNWCKey) -> NWCKey:
nwckey_entry = NWCKey(
pubkey=data.pubkey,
@ -34,29 +35,34 @@ async def create_nwc(data: CreateNWCKey) -> NWCKey:
pubkey=data.pubkey,
budget_msats=budget.budget_msats,
refresh_window=budget.refresh_window,
created_at=budget.created_at
created_at=budget.created_at,
)
await db.insert("nwcprovider.budgets", budget_entry)
return NWCKey(**data.dict())
async def delete_nwc(data:DeleteNWC) -> None:
async def delete_nwc(data: DeleteNWC) -> None:
await db.execute(
"DELETE FROM nwcprovider.keys WHERE pubkey = :pubkey AND wallet = :wallet", {"pubkey": data.pubkey, "wallet": data.wallet_id}
"DELETE FROM nwcprovider.keys WHERE pubkey = :pubkey AND wallet = :wallet",
{"pubkey": data.pubkey, "wallet": data.wallet_id},
)
async def get_wallet_nwcs(data: GetWalletNWC) -> List[NWCKey]:
return await db.fetchall(
"""
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},
{
"wallet": data.wallet_id,
"expires": int(time.time()) if not data.include_expired else -1,
},
model=NWCKey,
)
async def get_nwc(
data: GetNWCKey
) -> Optional[NWCKey]:
async def get_nwc(data: GetNWCKey) -> Optional[NWCKey]:
# expires_at = 0 means it never expires
if data.wallet_id:
row = await db.fetchone(
@ -65,8 +71,11 @@ async def get_nwc(
WHERE pubkey = :pubkey AND wallet = :wallet
AND (expires_at = 0 OR expires_at > :expires)
""",
{"pubkey": data.pubkey, "wallet": data.wallet_id,
"expires": int(time.time()) if not data.include_expired else -1},
{
"pubkey": data.pubkey,
"wallet": data.wallet_id,
"expires": int(time.time()) if not data.include_expired else -1,
},
NWCKey,
)
else:
@ -82,7 +91,10 @@ async def get_nwc(
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},
{
"pubkey": data.pubkey,
"expires": int(time.time()) if not data.include_expired else -1,
},
NWCKey,
)
if not row:
@ -90,16 +102,18 @@ async def get_nwc(
if data.refresh_last_used:
await db.execute(
"""
UPDATE nwcprovider.keys SET last_used = :last_used WHERE pubkey = :pubkey
UPDATE nwcprovider.keys SET last_used =
:last_used WHERE pubkey = :pubkey
""",
{"last_used":int(time.time()), "pubkey":data.pubkey},
{"last_used": int(time.time()), "pubkey": data.pubkey},
)
return NWCKey(**row)
async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
rows = await db.fetchall(
"SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey", {"pubkey":data.pubkey}
"SELECT * FROM nwcprovider.budgets WHERE pubkey = :pubkey",
{"pubkey": data.pubkey},
)
budgets = [NWCBudget(**row) for row in rows]
if data.calculate_spent:
@ -108,16 +122,21 @@ async def get_budgets_nwc(data: GetBudgetsNWC) -> Optional[NWCBudget]:
tot_spent_in_range_msats = await db.fetchone(
"""
SELECT SUM(amount_msats) FROM nwcprovider.spent
WHERE pubkey = :pubkey AND created_at >= :last_cycle AND created_at < next_cycle
WHERE pubkey = :pubkey AND created_at >=
:last_cycle AND created_at < next_cycle
""",
{"pubkey":data.pubkey, "last_cycle":last_cycle, "next_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(data:TrackedSpendNWC, action):
async def tracked_spend_nwc(data: TrackedSpendNWC, action):
async def r():
created_at = int(time.time())
budgets = await get_budgets_nwc(data.pubkey)
@ -129,9 +148,14 @@ async def tracked_spend_nwc(data:TrackedSpendNWC, action):
await db.fetchone(
"""
SELECT SUM(amount_msats) FROM nwcprovider.spent
WHERE pubkey = :pubkey AND created_at >= :last_cycle AND created_at < :next_cycle
WHERE pubkey = :pubkey AND created_at >=
:last_cycle AND created_at < :next_cycle
""",
{"pubkey":data.pubkey, "last_cycle":last_cycle, "next_cycle":next_cycle},
{
"pubkey": data.pubkey,
"last_cycle": last_cycle,
"next_cycle": next_cycle,
},
)
)[0]
or 0
@ -147,7 +171,11 @@ async def tracked_spend_nwc(data:TrackedSpendNWC, action):
INSERT INTO nwcprovider.spent (pubkey, amount_msats, created_at)
VALUES (:pubkey, :amount_msats, :created_at)
""",
{"pubkey":data.pubkey, "amount_msats":data.amount_msats, "created_at":created_at},
{
"pubkey": data.pubkey,
"amount_msats": data.amount_msats,
"created_at": created_at,
},
)
return True, out
@ -155,27 +183,31 @@ async def tracked_spend_nwc(data:TrackedSpendNWC, action):
async def get_config_nwc(key: str):
row = await db.fetchone("SELECT * FROM nwcprovider.config WHERE key = :key", {"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 set_config_nwc(key: str, value: str):
await db.execute(
"""
DELETE FROM nwcprovider.config
WHERE key = :key
""",
{"key":key},
{"key": key},
)
await db.execute(
"""
INSERT INTO nwcprovider.config (key, value)
VALUES (:key, :value)
""",
{"key":key, "value":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}

View file

@ -5,8 +5,10 @@ from sqlite3 import Row
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from .nwcp import NWCServiceProvider
class NWCKey(BaseModel):
pubkey: str
wallet: str
@ -26,37 +28,45 @@ 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
pubkey: str
@ -105,14 +115,17 @@ 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

View file

@ -76,7 +76,7 @@ class NWCServiceProvider:
self.request_listeners: Dict[
str,
Callable[
["NWCServiceProvider", str, Dict],
[NWCServiceProvider, str, Dict],
Awaitable[List[Tuple[Optional[Dict], Optional[Dict], List]]],
],
] = {}

2003
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,8 +6,8 @@ authors = ["Riccardo Balbo <oc@rblb.it>"]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = "*"
psycopg2-binary = "^2.9.9"
lnbits = {version = "*", allow-prereleases = true}
mypy = "^1.13.0"
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"
@ -16,27 +16,19 @@ pytest = "^7.3.2"
mypy = "^1.5.1"
pre-commit = "^3.2.2"
ruff = "^0.3.2"
types-cffi = "^1.16.0.20240331"
pytest-md = "^0.2.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]
exclude = "(tests/*)"
[[tool.mypy.overrides]]
module = [
"lnbits.*",
"lnurl.*",
"loguru.*",
"fastapi.*",
"pydantic.*",
"pyqrcode.*",
"shortuuid.*",
"httpx.*",
"websocket.*",
"secp256k1.*",
]
ignore_missing_imports = "True"
@ -52,9 +44,7 @@ line-length = 88
[tool.ruff]
# Same as Black. + 10% rule of black
line-length = 88
# exclude = [
# "nostr",
# ]
[tool.ruff.lint]
# Enable:
@ -69,7 +59,8 @@ line-length = 88
# RUF - ruff
# B - bugbear
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
ignore = ["C901"]
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
ignore = ["UP007"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
@ -86,7 +77,7 @@ classmethod-decorators = [
# Ignore unused imports in __init__.py files.
# [tool.ruff.lint.extend-per-file-ignores]
# "__init__.py" = ["F401", "F403"]
# "views_api.py" = ["F401"]
# [tool.ruff.lint.mccabe]
# max-complexity = 10

View file

@ -106,8 +106,8 @@ async def _process_invoice(
async def _on_pay_invoice(
data:OnInvoicePaid
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
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:
@ -136,7 +136,7 @@ async def _on_pay_invoice(
async def _on_multi_pay_invoice(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "multi_pay_invoice", data.payload)
@ -161,7 +161,7 @@ async def _on_multi_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:
@ -182,7 +182,7 @@ async def _on_multi_pay_invoice(
async def _on_make_invoice(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "make_invoice", data.payload)
@ -234,7 +234,7 @@ async def _on_make_invoice(
async def _on_lookup_invoice(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "lookup_invoice", data.payload)
@ -281,7 +281,7 @@ async def _on_lookup_invoice(
async def _on_list_transactions(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "list_transactions", data.payload)
@ -337,7 +337,7 @@ async def _on_list_transactions(
async def _on_get_balance(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "get_balance", data.payload)
@ -355,7 +355,7 @@ async def _on_get_balance(
async def _on_get_info(
data:OnInvoicePaid
data: OnInvoicePaid,
) -> List[Tuple[Optional[Dict], Optional[Dict], List]]:
nwc = await get_nwc(data.pubkey, None, False, True)
error = await _check(nwc, "get_info", data.payload)
@ -363,7 +363,7 @@ async def _on_get_info(
return [(None, error, [])]
if not nwc:
raise Exception("Pubkey has no associated wallet")
sp_methods = sp.get_supported_methods()
sp_methods = data.sp.get_supported_methods()
permissions = nwc.get_permissions()
# Filter only methods supported by the extension and allowed by the permissions
account_methods = []

View file

@ -38,8 +38,8 @@
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
window.app = Vue.createApp({
el: '#vue',
window.app = Vue.createApp({
el: "#vue",
mixins: [windowMixin],
delimiters: ["${", "}"],
data: function () {

View file

@ -99,7 +99,8 @@
<q-card-section>
<h6 class="text-subtitle1 q-my-none">NWC Service provider</h6>
<p>
Nostr Wallet Connect (NWC) is an open protocol to connect lightning wallets to apps
Nostr Wallet Connect (NWC) is an open protocol to connect lightning
wallets to apps
</p>
</q-card-section>
<q-card-section class="q-pa-none">
@ -407,8 +408,8 @@
window.NobleSecp256k1 = NobleSecp256k1;
</script>
<script>
window.app = Vue.createApp({
el: '#vue',
window.app = Vue.createApp({
el: "#vue",
mixins: [windowMixin],
delimiters: ["${", "}"],
data: function () {

View file

@ -18,13 +18,13 @@ from .crud import (
set_config_nwc,
)
from .models import (
NWCGetResponse,
RegisterNWC,
GetWalletNWC,
DeleteNWC,
GetBudgetsNWC,
GetNWCKey,
GetNWCs,
DeleteNWC
GetWalletNWC,
NWCGetResponse,
RegisterNWC,
)
from .permission import nwc_permissions
@ -50,16 +50,14 @@ async def api_get_nwcs(
wallet_id = wallet.wallet.id
wallet_nwcs = GetWalletNWC(
wallet_id=wallet_id,
include_expired=data.include_expired
)
wallet_id=wallet_id, include_expired=data.include_expired
)
nwcs = await get_wallet_nwcs(wallet_nwcs)
out = []
for nwc in nwcs:
budgets_nwc = GetBudgetsNWC(
pubkey=nwc.pubkey,
calculate_spent=data.calculate_spent_budget
)
pubkey=nwc.pubkey, calculate_spent=data.calculate_spent_budget
)
budgets = await get_budgets_nwc(budgets_nwc)
res = NWCGetResponse(data=nwc, budgets=budgets)
out.append(res)
@ -71,9 +69,8 @@ async def api_get_nwcs(
"/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK, response_model=NWCGetResponse
)
async def api_get_nwc(
data:GetNWCKey,
wallet: WalletTypeInfo = Depends(require_admin_key)
) -> NWCGetResponse:
data: GetNWCKey, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> NWCGetResponse:
wallet_id = wallet.wallet.id
nwc = await get_nwc(data.pubkey, wallet_id, data.include_expired)
if not nwc:
@ -126,7 +123,7 @@ async def api_get_pairing_url(req: Request, secret: str) -> str:
response_model=NWCGetResponse,
)
async def api_register_nwc(
data:RegisterNWC, # Use the Pydantic model here
data: RegisterNWC, # Use the Pydantic model here
wallet: WalletTypeInfo = Depends(require_admin_key),
):
wallet_id = wallet.wallet.id
@ -146,11 +143,13 @@ async def api_register_nwc(
# Delete a nwc key
@nwcprovider_api_router.delete("/api/v1/nwc/{pubkey}", status_code=HTTPStatus.OK)
async def api_delete_nwc(
data:DeleteNWC, wallet: WalletTypeInfo = Depends(require_admin_key)
data: DeleteNWC, wallet: WalletTypeInfo = Depends(require_admin_key)
):
wallet_id = wallet.wallet.id
await delete_nwc(data.pubkey, wallet_id)
return JSONResponse(content={"message": f"NWC key {data.pubkey} deleted successfully."})
return JSONResponse(
content={"message": f"NWC key {data.pubkey} deleted successfully."}
)
# Get config