Skip to content

Commit d72cf40

Browse files
motorina0dni
andauthored
[feat] Pay to enable extension (lnbits#2516)
* feat: add payment tab * feat: add buttons * feat: persist `pay to enable` changes * fix: do not disable extension on upgrade * fix: show releases tab first * feat: extract `enableExtension` logic * refactor: rename routes * feat: show dialog for paying extension * feat: create invoice to enable * refactor: extract enable/disable extension logic * feat: add extra info to UserExtensions * feat: check payment for extension enable * fix: parsing * feat: admins must not pay * fix: code checks * fix: test * refactor: extract extension activate/deactivate to the `api` side * feat: add `get_user_extensions ` * feat: return explicit `requiresPayment` * feat: add `isPaymentRequired` to extension list * fix: `paid_to_enable` status * fix: ui layout * feat: show QR Code * feat: wait for invoice to be paid * test: removed deprecated test and dead code * feat: add re-check button * refactor: rename paths for endpoints * feat: i18n * feat: add `{"success": True}` * test: fix listener * fix: rebase errors * chore: update bundle * fix: return error status code for the HTML error pages * fix: active extension loading from file system * chore: temp commit * fix: premature optimisation * chore: make check * refactor: remove extracted logic * chore: code format * fix: enable by default after install * fix: use `discard` instead of `remove` for `set` * chore: code format * fix: better error code * fix: check for stop function before invoking * feat: check if the wallet belongs to the admin user * refactor: return 402 Requires Payment * chore: more typing * chore: temp checkout different branch for tests * fix: too much typing * fix: remove try-except * fix: typo * fix: manual format * fix: merge issue * remove this line --------- Co-authored-by: dni ⚡ <[email protected]>
1 parent 7c68a02 commit d72cf40

16 files changed

+785
-189
lines changed

lnbits/app.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
from slowapi.util import get_remote_address
1717
from starlette.middleware.sessions import SessionMiddleware
1818

19-
from lnbits.core.crud import get_dbversions, get_installed_extensions
19+
from lnbits.core.crud import (
20+
get_dbversions,
21+
get_installed_extensions,
22+
update_installed_extension_state,
23+
)
2024
from lnbits.core.helpers import migrate_extension_database
2125
from lnbits.core.tasks import ( # watchdog_task
2226
killswitch_task,
@@ -42,7 +46,6 @@
4246
from .core.db import core_app_extra
4347
from .core.services import check_admin_settings, check_webpush_settings
4448
from .core.views.extension_api import add_installed_extension
45-
from .core.views.generic import update_installed_extension_state
4649
from .extension_manager import (
4750
Extension,
4851
InstallableExtension,

lnbits/commands.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
delete_wallet_by_id,
3030
delete_wallet_payment,
3131
get_dbversions,
32-
get_inactive_extensions,
3332
get_installed_extension,
3433
get_installed_extensions,
3534
get_payments,
@@ -154,6 +153,7 @@ async def migrate_databases():
154153
# `installed_extensions` table has been created
155154
await load_disabled_extension_list()
156155

156+
# todo: revisit, use installed extensions
157157
for ext in get_valid_extensions(False):
158158
current_version = current_versions.get(ext.code, 0)
159159
try:
@@ -315,8 +315,8 @@ async def check_invalid_payments(
315315

316316
async def load_disabled_extension_list() -> None:
317317
"""Update list of extensions that have been explicitly disabled"""
318-
inactive_extensions = await get_inactive_extensions()
319-
settings.lnbits_deactivated_extensions.update(inactive_extensions)
318+
inactive_extensions = await get_installed_extensions(active=False)
319+
settings.lnbits_deactivated_extensions.update([e.id for e in inactive_extensions])
320320

321321

322322
@extensions.command("list")

lnbits/core/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .views.extension_api import extension_router
88

99
# this compat is needed for usermanager extension
10-
from .views.generic import generic_router, update_user_extension
10+
from .views.generic import generic_router
1111
from .views.node_api import node_router, public_node_router, super_node_router
1212
from .views.payment_api import payment_router
1313
from .views.public_api import public_router

lnbits/core/crud.py

+67-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99

1010
from lnbits.core.db import db
1111
from lnbits.db import DB_TYPE, SQLITE, Connection, Database, Filters, Page
12-
from lnbits.extension_manager import InstallableExtension
12+
from lnbits.extension_manager import (
13+
InstallableExtension,
14+
PayToEnableInfo,
15+
UserExtension,
16+
UserExtensionInfo,
17+
)
1318
from lnbits.settings import (
1419
AdminSettings,
1520
EditableSettings,
@@ -364,6 +369,7 @@ async def add_installed_extension(
364369
"installed_release": (
365370
dict(ext.installed_release) if ext.installed_release else None
366371
),
372+
"pay_to_enable": (dict(ext.pay_to_enable) if ext.pay_to_enable else None),
367373
"dependencies": ext.dependencies,
368374
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
369375
}
@@ -373,22 +379,23 @@ async def add_installed_extension(
373379
await (conn or db).execute(
374380
"""
375381
INSERT INTO installed_extensions
376-
(id, version, name, short_description, icon, stars, meta)
377-
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
382+
(id, version, name, active, short_description, icon, stars, meta)
383+
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
378384
(version, name, active, short_description, icon, stars, meta) =
379385
(?, ?, ?, ?, ?, ?, ?)
380386
""",
381387
(
382388
ext.id,
383389
version,
384390
ext.name,
391+
ext.active,
385392
ext.short_description,
386393
ext.icon,
387394
ext.stars,
388395
json.dumps(meta),
389396
version,
390397
ext.name,
391-
False,
398+
ext.active,
392399
ext.short_description,
393400
ext.icon,
394401
ext.stars,
@@ -408,6 +415,17 @@ async def update_installed_extension_state(
408415
)
409416

410417

418+
async def update_extension_pay_to_enable(
419+
ext_id: str, payment_info: PayToEnableInfo, conn: Optional[Connection] = None
420+
) -> None:
421+
ext = await get_installed_extension(ext_id, conn)
422+
if not ext:
423+
return
424+
ext.pay_to_enable = payment_info
425+
426+
await add_installed_extension(ext, conn)
427+
428+
411429
async def delete_installed_extension(
412430
*, ext_id: str, conn: Optional[Connection] = None
413431
) -> None:
@@ -450,21 +468,44 @@ async def get_installed_extension(
450468

451469

452470
async def get_installed_extensions(
471+
active: Optional[bool] = None,
453472
conn: Optional[Connection] = None,
454473
) -> List["InstallableExtension"]:
455474
rows = await (conn or db).fetchall(
456475
"SELECT * FROM installed_extensions",
457476
(),
458477
)
459-
return [InstallableExtension.from_row(row) for row in rows]
478+
all_extensions = [InstallableExtension.from_row(row) for row in rows]
479+
if active is None:
480+
return all_extensions
460481

482+
return [e for e in all_extensions if e.active == active]
461483

462-
async def get_inactive_extensions(*, conn: Optional[Connection] = None) -> List[str]:
463-
inactive_extensions = await (conn or db).fetchall(
464-
"""SELECT id FROM installed_extensions WHERE NOT active""",
465-
(),
484+
485+
async def get_user_extension(
486+
user_id: str, extension: str, conn: Optional[Connection] = None
487+
) -> Optional[UserExtension]:
488+
row = await (conn or db).fetchone(
489+
"""
490+
SELECT extension, active, extra as _extra FROM extensions
491+
WHERE "user" = ? AND extension = ?
492+
""",
493+
(user_id, extension),
494+
)
495+
return UserExtension.from_row(row) if row else None
496+
497+
498+
async def get_user_extensions(
499+
user_id: str, conn: Optional[Connection] = None
500+
) -> List[UserExtension]:
501+
rows = await (conn or db).fetchall(
502+
"""
503+
SELECT extension, active, extra as _extra FROM extensions
504+
WHERE "user" = ?
505+
""",
506+
(user_id,),
466507
)
467-
return [ext[0] for ext in inactive_extensions]
508+
return [UserExtension.from_row(row) for row in rows]
468509

469510

470511
async def update_user_extension(
@@ -489,6 +530,22 @@ async def get_user_active_extensions_ids(
489530
return [e[0] for e in rows]
490531

491532

533+
async def update_user_extension_extra(
534+
user_id: str,
535+
extension: str,
536+
extra: UserExtensionInfo,
537+
conn: Optional[Connection] = None,
538+
) -> None:
539+
extra_json = json.dumps(dict(extra))
540+
await (conn or db).execute(
541+
"""
542+
INSERT INTO extensions ("user", extension, extra) VALUES (?, ?, ?)
543+
ON CONFLICT ("user", extension) DO UPDATE SET extra = ?
544+
""",
545+
(user_id, extension, extra_json, extra_json),
546+
)
547+
548+
492549
# wallets
493550
# -------
494551

lnbits/core/helpers.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ async def _stop_extension_background_work(ext_id) -> bool:
7777
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
7878
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
7979

80-
await getattr(old_module, stop_fn_name)()
80+
stop_fn = getattr(old_module, stop_fn_name)
81+
if stop_fn:
82+
await stop_fn()
8183

8284
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
8385
except Exception as ex:

lnbits/core/migrations.py

+7
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,10 @@ async def m019_balances_view_based_on_wallets(db):
513513
GROUP BY apipayments.wallet
514514
"""
515515
)
516+
517+
518+
async def m020_add_column_column_to_user_extensions(db):
519+
"""
520+
Adds extra column to user extensions.
521+
"""
522+
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")

lnbits/core/services.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
651651

652652
async def send_payment_notification(wallet: Wallet, payment: Payment):
653653
await websocket_updater(
654-
wallet.id,
654+
wallet.inkey,
655655
json.dumps(
656656
{
657657
"wallet_balance": wallet.balance,
@@ -660,6 +660,10 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
660660
),
661661
)
662662

663+
await websocket_updater(
664+
payment.payment_hash, json.dumps({"pending": payment.pending})
665+
)
666+
663667

664668
async def update_wallet_balance(wallet_id: str, amount: int):
665669
payment_hash, _ = await create_invoice(

0 commit comments

Comments
 (0)