migrate to sqlalchemy-aio.

a big refactor that:

- fixes some issues that might have happened (or not) with asynchronous
    reactions to payments;
- paves the way to https://github.com/lnbits/lnbits/issues/121;
- uses more async/await notation which just looks nice; and
- makes it simple(r?) for one extension to modify stuff from other extensions.
This commit is contained in:
fiatjaf 2020-11-21 18:04:39 -03:00
parent f877dde2b0
commit d3fc52cd49
68 changed files with 971 additions and 1075 deletions

View File

@ -12,6 +12,8 @@ black: $(shell find lnbits -name "*.py")
mypy: $(shell find lnbits -name "*.py") mypy: $(shell find lnbits -name "*.py")
./venv/bin/mypy lnbits ./venv/bin/mypy lnbits
./venv/bin/mypy lnbits/core
./venv/bin/mypy lnbits/extensions/*
checkprettier: $(shell find lnbits -name "*.js" -name ".html") checkprettier: $(shell find lnbits -name "*.js" -name ".html")
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js ./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js

View File

@ -1,13 +1,17 @@
from .app import create_app import trio # type: ignore
from .commands import migrate_databases, transpile_scss, bundle_vendored
from .settings import LNBITS_SITE_TITLE, SERVICE_FEE, DEBUG, LNBITS_DATA_FOLDER, WALLET, LNBITS_COMMIT
migrate_databases() from .commands import migrate_databases, transpile_scss, bundle_vendored
trio.run(migrate_databases)
transpile_scss() transpile_scss()
bundle_vendored() bundle_vendored()
from .app import create_app
app = create_app() app = create_app()
from .settings import LNBITS_SITE_TITLE, SERVICE_FEE, DEBUG, LNBITS_DATA_FOLDER, WALLET, LNBITS_COMMIT
print( print(
f"""Starting LNbits with f"""Starting LNbits with
- git version: {LNBITS_COMMIT} - git version: {LNBITS_COMMIT}

View File

@ -9,7 +9,6 @@ from secure import SecureHeaders # type: ignore
from .commands import db_migrate, handle_assets from .commands import db_migrate, handle_assets
from .core import core_app from .core import core_app
from .db import open_db, open_ext_db
from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
from .proxy_fix import ASGIProxyFix from .proxy_fix import ASGIProxyFix
from .tasks import run_deferred_async, invoice_listener, internal_invoice_listener, webhook_handler, grab_app_for_later from .tasks import run_deferred_async, invoice_listener, internal_invoice_listener, webhook_handler, grab_app_for_later
@ -63,13 +62,9 @@ def register_blueprints(app: QuartTrio) -> None:
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}") ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
bp = getattr(ext_module, f"{ext.code}_ext") bp = getattr(ext_module, f"{ext.code}_ext")
@bp.before_request
async def before_request():
g.ext_db = open_ext_db(ext.code)
@bp.teardown_request @bp.teardown_request
async def after_request(exc): async def after_request(exc):
g.ext_db.close() await ext_module.db.close_session()
app.register_blueprint(bp, url_prefix=f"/{ext.code}") app.register_blueprint(bp, url_prefix=f"/{ext.code}")
except Exception: except Exception:
@ -106,18 +101,19 @@ def register_request_hooks(app: QuartTrio):
@app.before_request @app.before_request
async def before_request(): async def before_request():
g.db = open_db()
g.nursery = app.nursery g.nursery = app.nursery
@app.teardown_request
async def after_request(exc):
from lnbits.core import db
await db.close_session()
@app.after_request @app.after_request
async def set_secure_headers(response): async def set_secure_headers(response):
secure_headers.quart(response) secure_headers.quart(response)
return response return response
@app.teardown_request
async def after_request(exc):
g.db.close()
def register_async_tasks(app): def register_async_tasks(app):
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) @app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])

View File

@ -1,19 +1,19 @@
import trio # type: ignore
import warnings import warnings
import click import click
import importlib import importlib
import re import re
import os import os
import sqlite3 from sqlalchemy.exc import OperationalError # type: ignore
from .core import migrations as core_migrations from .core import db as core_db, migrations as core_migrations
from .db import open_db, open_ext_db
from .helpers import get_valid_extensions, get_css_vendored, get_js_vendored, url_for_vendored from .helpers import get_valid_extensions, get_css_vendored, get_js_vendored, url_for_vendored
from .settings import LNBITS_PATH from .settings import LNBITS_PATH
@click.command("migrate") @click.command("migrate")
def db_migrate(): def db_migrate():
migrate_databases() trio.run(migrate_databases)
@click.command("assets") @click.command("assets")
@ -45,39 +45,44 @@ def bundle_vendored():
f.write(output) f.write(output)
def migrate_databases(): async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them.""" """Creates the necessary databases if they don't exist already; or migrates them."""
with open_db() as core_db: core_conn = await core_db.connect()
core_txn = await core_conn.begin()
try:
rows = await (await core_conn.execute("SELECT * FROM dbversions")).fetchall()
except OperationalError:
# migration 3 wasn't ran
core_migrations.m000_create_migrations_table(core_conn)
rows = await (await core_conn.execute("SELECT * FROM dbversions")).fetchall()
current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_")
async def run_migration(db, migrations_module):
db_name = migrations_module.__name__.split(".")[-2]
for key, migrate in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
await migrate(db)
await core_conn.execute(
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version)
)
await run_migration(core_conn, core_migrations)
for ext in get_valid_extensions():
try: try:
rows = core_db.fetchall("SELECT * FROM dbversions") ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
except sqlite3.OperationalError: ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
# migration 3 wasn't ran await run_migration(ext_db, ext_migrations)
core_migrations.m000_create_migrations_table(core_db) except ImportError:
rows = core_db.fetchall("SELECT * FROM dbversions") raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")
current_versions = {row["db"]: row["version"] for row in rows} await core_txn.commit()
matcher = re.compile(r"^m(\d\d\d)_") await core_conn.close()
def run_migration(db, migrations_module):
db_name = migrations_module.__name__.split(".")[-2]
for key, run_migration in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
run_migration(db)
core_db.execute(
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version)
)
run_migration(core_db, core_migrations)
for ext in get_valid_extensions():
try:
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations")
with open_ext_db(ext.code) as db:
run_migration(db, ext_migrations)
except ImportError:
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.")

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("database")
core_app: Blueprint = Blueprint( core_app: Blueprint = Blueprint(
"core", __name__, template_folder="templates", static_folder="static", static_url_path="/core/static" "core", __name__, template_folder="templates", static_folder="static", static_url_path="/core/static"

View File

@ -2,11 +2,11 @@ import json
import datetime import datetime
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, Dict from typing import List, Optional, Dict
from quart import g
from lnbits import bolt11 from lnbits import bolt11
from lnbits.settings import DEFAULT_WALLET_NAME from lnbits.settings import DEFAULT_WALLET_NAME
from . import db
from .models import User, Wallet, Payment from .models import User, Wallet, Payment
@ -14,28 +14,28 @@ from .models import User, Wallet, Payment
# -------- # --------
def create_account() -> User: async def create_account() -> User:
user_id = uuid4().hex user_id = uuid4().hex
g.db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,)) await db.execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = get_account(user_id=user_id) new_account = await get_account(user_id=user_id)
assert new_account, "Newly created account couldn't be retrieved" assert new_account, "Newly created account couldn't be retrieved"
return new_account return new_account
def get_account(user_id: str) -> Optional[User]: async def get_account(user_id: str) -> Optional[User]:
row = g.db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)) row = await db.fetchone("SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,))
return User(**row) if row else None return User(**row) if row else None
def get_user(user_id: str) -> Optional[User]: async def get_user(user_id: str) -> Optional[User]:
user = g.db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,)) user = await db.fetchone("SELECT id, email FROM accounts WHERE id = ?", (user_id,))
if user: if user:
extensions = g.db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,)) extensions = await db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,))
wallets = g.db.fetchall( wallets = await db.fetchall(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets FROM wallets
@ -51,8 +51,8 @@ def get_user(user_id: str) -> Optional[User]:
) )
def update_user_extension(*, user_id: str, extension: str, active: int) -> None: async def update_user_extension(*, user_id: str, extension: str, active: int) -> None:
g.db.execute( await db.execute(
""" """
INSERT OR REPLACE INTO extensions (user, extension, active) INSERT OR REPLACE INTO extensions (user, extension, active)
VALUES (?, ?, ?) VALUES (?, ?, ?)
@ -65,9 +65,9 @@ def update_user_extension(*, user_id: str, extension: str, active: int) -> None:
# ------- # -------
def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> Wallet: async def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> Wallet:
wallet_id = uuid4().hex wallet_id = uuid4().hex
g.db.execute( await db.execute(
""" """
INSERT INTO wallets (id, name, user, adminkey, inkey) INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
@ -75,14 +75,14 @@ def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> Wallet:
(wallet_id, wallet_name or DEFAULT_WALLET_NAME, user_id, uuid4().hex, uuid4().hex), (wallet_id, wallet_name or DEFAULT_WALLET_NAME, user_id, uuid4().hex, uuid4().hex),
) )
new_wallet = get_wallet(wallet_id=wallet_id) new_wallet = await get_wallet(wallet_id=wallet_id)
assert new_wallet, "Newly created wallet couldn't be retrieved" assert new_wallet, "Newly created wallet couldn't be retrieved"
return new_wallet return new_wallet
def delete_wallet(*, user_id: str, wallet_id: str) -> None: async def delete_wallet(*, user_id: str, wallet_id: str) -> None:
g.db.execute( await db.execute(
""" """
UPDATE wallets AS w UPDATE wallets AS w
SET SET
@ -95,8 +95,8 @@ def delete_wallet(*, user_id: str, wallet_id: str) -> None:
) )
def get_wallet(wallet_id: str) -> Optional[Wallet]: async def get_wallet(wallet_id: str) -> Optional[Wallet]:
row = g.db.fetchone( row = await db.fetchone(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets FROM wallets
@ -108,8 +108,8 @@ def get_wallet(wallet_id: str) -> Optional[Wallet]:
return Wallet(**row) if row else None return Wallet(**row) if row else None
def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]: async def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
row = g.db.fetchone( row = await db.fetchone(
""" """
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets FROM wallets
@ -131,8 +131,8 @@ def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wallet]:
# --------------- # ---------------
def get_standalone_payment(checking_id: str) -> Optional[Payment]: async def get_standalone_payment(checking_id: str) -> Optional[Payment]:
row = g.db.fetchone( row = await db.fetchone(
""" """
SELECT * SELECT *
FROM apipayments FROM apipayments
@ -144,8 +144,8 @@ def get_standalone_payment(checking_id: str) -> Optional[Payment]:
return Payment.from_row(row) if row else None return Payment.from_row(row) if row else None
def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]: async def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]:
row = g.db.fetchone( row = await db.fetchone(
""" """
SELECT * SELECT *
FROM apipayments FROM apipayments
@ -157,7 +157,7 @@ def get_wallet_payment(wallet_id: str, payment_hash: str) -> Optional[Payment]:
return Payment.from_row(row) if row else None return Payment.from_row(row) if row else None
def get_wallet_payments( async def get_wallet_payments(
wallet_id: str, wallet_id: str,
*, *,
complete: bool = False, complete: bool = False,
@ -197,7 +197,7 @@ def get_wallet_payments(
clause += "AND checking_id NOT LIKE 'temp_%' " clause += "AND checking_id NOT LIKE 'temp_%' "
clause += "AND checking_id NOT LIKE 'internal_%' " clause += "AND checking_id NOT LIKE 'internal_%' "
rows = g.db.fetchall( rows = await db.fetchall(
f""" f"""
SELECT * SELECT *
FROM apipayments FROM apipayments
@ -210,8 +210,8 @@ def get_wallet_payments(
return [Payment.from_row(row) for row in rows] return [Payment.from_row(row) for row in rows]
def delete_expired_invoices() -> None: async def delete_expired_invoices() -> None:
rows = g.db.fetchall( rows = await db.fetchall(
""" """
SELECT bolt11 SELECT bolt11
FROM apipayments FROM apipayments
@ -228,7 +228,7 @@ def delete_expired_invoices() -> None:
if expiration_date > datetime.datetime.utcnow(): if expiration_date > datetime.datetime.utcnow():
continue continue
g.db.execute( await db.execute(
""" """
DELETE FROM apipayments DELETE FROM apipayments
WHERE pending = 1 AND hash = ? WHERE pending = 1 AND hash = ?
@ -241,7 +241,7 @@ def delete_expired_invoices() -> None:
# -------- # --------
def create_payment( async def create_payment(
*, *,
wallet_id: str, wallet_id: str,
checking_id: str, checking_id: str,
@ -254,7 +254,7 @@ def create_payment(
pending: bool = True, pending: bool = True,
extra: Optional[Dict] = None, extra: Optional[Dict] = None,
) -> Payment: ) -> Payment:
g.db.execute( await db.execute(
""" """
INSERT INTO apipayments INSERT INTO apipayments
(wallet, checking_id, bolt11, hash, preimage, (wallet, checking_id, bolt11, hash, preimage,
@ -275,14 +275,14 @@ def create_payment(
), ),
) )
new_payment = get_wallet_payment(wallet_id, payment_hash) new_payment = await get_wallet_payment(wallet_id, payment_hash)
assert new_payment, "Newly created payment couldn't be retrieved" assert new_payment, "Newly created payment couldn't be retrieved"
return new_payment return new_payment
def update_payment_status(checking_id: str, pending: bool) -> None: async def update_payment_status(checking_id: str, pending: bool) -> None:
g.db.execute( await db.execute(
"UPDATE apipayments SET pending = ? WHERE checking_id = ?", "UPDATE apipayments SET pending = ? WHERE checking_id = ?",
( (
int(pending), int(pending),
@ -291,12 +291,12 @@ def update_payment_status(checking_id: str, pending: bool) -> None:
) )
def delete_payment(checking_id: str) -> None: async def delete_payment(checking_id: str) -> None:
g.db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)) await db.execute("DELETE FROM apipayments WHERE checking_id = ?", (checking_id,))
def check_internal(payment_hash: str) -> Optional[str]: async def check_internal(payment_hash: str) -> Optional[str]:
row = g.db.fetchone( row = await db.fetchone(
""" """
SELECT checking_id FROM apipayments SELECT checking_id FROM apipayments
WHERE hash = ? AND pending AND amount > 0 WHERE hash = ? AND pending AND amount > 0

View File

@ -1,8 +1,8 @@
import sqlite3 from sqlalchemy.exc import OperationalError # type: ignore
def m000_create_migrations_table(db): async def m000_create_migrations_table(db):
db.execute( await db.execute(
""" """
CREATE TABLE dbversions ( CREATE TABLE dbversions (
db TEXT PRIMARY KEY, db TEXT PRIMARY KEY,
@ -12,11 +12,11 @@ def m000_create_migrations_table(db):
) )
def m001_initial(db): async def m001_initial(db):
""" """
Initial LNbits tables. Initial LNbits tables.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -25,7 +25,7 @@ def m001_initial(db):
); );
""" """
) )
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS extensions ( CREATE TABLE IF NOT EXISTS extensions (
user TEXT NOT NULL, user TEXT NOT NULL,
@ -36,7 +36,7 @@ def m001_initial(db):
); );
""" """
) )
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS wallets ( CREATE TABLE IF NOT EXISTS wallets (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -47,7 +47,7 @@ def m001_initial(db):
); );
""" """
) )
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS apipayments ( CREATE TABLE IF NOT EXISTS apipayments (
payhash TEXT NOT NULL, payhash TEXT NOT NULL,
@ -63,7 +63,7 @@ def m001_initial(db):
""" """
) )
db.execute( await db.execute(
""" """
CREATE VIEW IF NOT EXISTS balances AS CREATE VIEW IF NOT EXISTS balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM ( SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
@ -82,22 +82,22 @@ def m001_initial(db):
) )
def m002_add_fields_to_apipayments(db): async def m002_add_fields_to_apipayments(db):
""" """
Adding fields to apipayments for better accounting, Adding fields to apipayments for better accounting,
and renaming payhash to checking_id since that is what it really is. and renaming payhash to checking_id since that is what it really is.
""" """
try: try:
db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id") await db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id")
db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT") await db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT")
db.execute("CREATE INDEX by_hash ON apipayments (hash)") await db.execute("CREATE INDEX by_hash ON apipayments (hash)")
db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT") await db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT") await db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT") await db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
import json import json
rows = db.fetchall("SELECT * FROM apipayments") rows = await (await db.execute("SELECT * FROM apipayments")).fetchall()
for row in rows: for row in rows:
if not row["memo"] or not row["memo"].startswith("#"): if not row["memo"] or not row["memo"].startswith("#"):
continue continue
@ -106,7 +106,7 @@ def m002_add_fields_to_apipayments(db):
prefix = "#" + ext + " " prefix = "#" + ext + " "
if row["memo"].startswith(prefix): if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :] new = row["memo"][len(prefix) :]
db.execute( await db.execute(
""" """
UPDATE apipayments SET extra = ?, memo = ? UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ? WHERE checking_id = ? AND memo = ?
@ -114,7 +114,7 @@ def m002_add_fields_to_apipayments(db):
(json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]), (json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]),
) )
break break
except sqlite3.OperationalError: except OperationalError:
# this is necessary now because it may be the case that this migration will # this is necessary now because it may be the case that this migration will
# run twice in some environments. # run twice in some environments.
# catching errors like this won't be necessary in anymore now that we # catching errors like this won't be necessary in anymore now that we

View File

@ -40,18 +40,14 @@ class Wallet(NamedTuple):
hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest() hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest()
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256") linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
return SigningKey.from_string( return SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=hashlib.sha256,)
linking_key,
curve=SECP256k1,
hashfunc=hashlib.sha256,
)
def get_payment(self, payment_hash: str) -> Optional["Payment"]: async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment from .crud import get_wallet_payment
return get_wallet_payment(self.id, payment_hash) return await get_wallet_payment(self.id, payment_hash)
def get_payments( async def get_payments(
self, self,
*, *,
complete: bool = True, complete: bool = True,
@ -62,7 +58,7 @@ class Wallet(NamedTuple):
) -> List["Payment"]: ) -> List["Payment"]:
from .crud import get_wallet_payments from .crud import get_wallet_payments
return get_wallet_payments( return await get_wallet_payments(
self.id, self.id,
complete=complete, complete=complete,
pending=pending, pending=pending,
@ -125,12 +121,12 @@ class Payment(NamedTuple):
def is_uncheckable(self) -> bool: def is_uncheckable(self) -> bool:
return self.checking_id.startswith("temp_") or self.checking_id.startswith("internal_") return self.checking_id.startswith("temp_") or self.checking_id.startswith("internal_")
def set_pending(self, pending: bool) -> None: async def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status from .crud import update_payment_status
update_payment_status(self.checking_id, pending) await update_payment_status(self.checking_id, pending)
def check_pending(self) -> None: async def check_pending(self) -> None:
if self.is_uncheckable: if self.is_uncheckable:
return return
@ -139,9 +135,9 @@ class Payment(NamedTuple):
else: else:
pending = WALLET.get_invoice_status(self.checking_id) pending = WALLET.get_invoice_status(self.checking_id)
self.set_pending(pending.pending) await self.set_pending(pending.pending)
def delete(self) -> None: async def delete(self) -> None:
from .crud import delete_payment from .crud import delete_payment
delete_payment(self.checking_id) await delete_payment(self.checking_id)

View File

@ -1,4 +1,3 @@
import trio # type: ignore
import json import json
import httpx import httpx
from io import BytesIO from io import BytesIO
@ -18,10 +17,11 @@ from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus, PaymentResponse from lnbits.wallets.base import PaymentStatus, PaymentResponse
from . import db
from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment from .crud import get_wallet, create_payment, delete_payment, check_internal, update_payment_status, get_wallet_payment
def create_invoice( async def create_invoice(
*, *,
wallet_id: str, wallet_id: str,
amount: int, # in satoshis amount: int, # in satoshis
@ -29,6 +29,7 @@ def create_invoice(
description_hash: Optional[bytes] = None, description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None, extra: Optional[Dict] = None,
) -> Tuple[str, str]: ) -> Tuple[str, str]:
await db.begin()
invoice_memo = None if description_hash else memo invoice_memo = None if description_hash else memo
storeable_memo = memo storeable_memo = memo
@ -41,7 +42,7 @@ def create_invoice(
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
amount_msat = amount * 1000 amount_msat = amount * 1000
create_payment( await create_payment(
wallet_id=wallet_id, wallet_id=wallet_id,
checking_id=checking_id, checking_id=checking_id,
payment_request=payment_request, payment_request=payment_request,
@ -51,11 +52,11 @@ def create_invoice(
extra=extra, extra=extra,
) )
g.db.commit() await db.commit()
return invoice.payment_hash, payment_request return invoice.payment_hash, payment_request
def pay_invoice( async def pay_invoice(
*, *,
wallet_id: str, wallet_id: str,
payment_request: str, payment_request: str,
@ -63,6 +64,7 @@ def pay_invoice(
extra: Optional[Dict] = None, extra: Optional[Dict] = None,
description: str = "", description: str = "",
) -> str: ) -> str:
await db.begin()
temp_id = f"temp_{urlsafe_short_hash()}" temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}"
@ -94,58 +96,53 @@ def pay_invoice(
) )
# check_internal() returns the checking_id of the invoice we're waiting for # check_internal() returns the checking_id of the invoice we're waiting for
internal_checking_id = check_internal(invoice.payment_hash) internal_checking_id = await check_internal(invoice.payment_hash)
if internal_checking_id: if internal_checking_id:
# create a new payment from this wallet # create a new payment from this wallet
create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs) await create_payment(checking_id=internal_id, fee=0, pending=False, **payment_kwargs)
else: else:
# create a temporary payment here so we can check if # create a temporary payment here so we can check if
# the balance is enough in the next step # the balance is enough in the next step
fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) fee_reserve = max(1000, int(invoice.amount_msat * 0.01))
create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs) await create_payment(checking_id=temp_id, fee=-fee_reserve, **payment_kwargs)
# do the balance check # do the balance check
wallet = get_wallet(wallet_id) wallet = await get_wallet(wallet_id)
assert wallet assert wallet
if wallet.balance_msat < 0: if wallet.balance_msat < 0:
g.db.rollback() await db.rollback()
raise PermissionError("Insufficient balance.") raise PermissionError("Insufficient balance.")
else: else:
g.db.commit() await db.commit()
await db.begin()
if internal_checking_id: if internal_checking_id:
# mark the invoice from the other side as not pending anymore # mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure # so the other side only has access to his new money when we are sure
# the payer has enough to deduct from # the payer has enough to deduct from
update_payment_status(checking_id=internal_checking_id, pending=False) await update_payment_status(checking_id=internal_checking_id, pending=False)
# notify receiver asynchronously # notify receiver asynchronously
from lnbits.tasks import internal_invoice_paid from lnbits.tasks import internal_invoice_paid
try: await internal_invoice_paid.send(internal_checking_id)
internal_invoice_paid.send_nowait(internal_checking_id)
except trio.WouldBlock:
pass
else: else:
# actually pay the external invoice # actually pay the external invoice
payment: PaymentResponse = WALLET.pay_invoice(payment_request) payment: PaymentResponse = WALLET.pay_invoice(payment_request)
if payment.ok and payment.checking_id: if payment.ok and payment.checking_id:
create_payment( await create_payment(
checking_id=payment.checking_id, checking_id=payment.checking_id, fee=payment.fee_msat, preimage=payment.preimage, **payment_kwargs,
fee=payment.fee_msat,
preimage=payment.preimage,
**payment_kwargs,
) )
delete_payment(temp_id) await delete_payment(temp_id)
else: else:
raise Exception(payment.error_message or "Failed to pay_invoice on backend.") raise Exception(payment.error_message or "Failed to pay_invoice on backend.")
g.db.commit() await db.commit()
return invoice.payment_hash return invoice.payment_hash
async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None: async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo: Optional[str] = None) -> None:
_, payment_request = create_invoice( _, payment_request = await create_invoice(
wallet_id=wallet_id, wallet_id=wallet_id,
amount=res.max_sats, amount=res.max_sats,
memo=memo or res.default_description or "", memo=memo or res.default_description or "",
@ -154,8 +151,7 @@ async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
await client.get( await client.get(
res.callback.base, res.callback.base, params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}},
) )
@ -212,11 +208,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get( r = await client.get(
callback, callback,
params={ params={"k1": k1.hex(), "key": key.verifying_key.to_string("compressed").hex(), "sig": sig.hex(),},
"k1": k1.hex(),
"key": key.verifying_key.to_string("compressed").hex(),
"sig": sig.hex(),
},
) )
try: try:
resp = json.loads(r.text) resp = json.loads(r.text)
@ -225,13 +217,11 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]:
return LnurlErrorResponse(reason=resp["reason"]) return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError): except (KeyError, json.decoder.JSONDecodeError):
return LnurlErrorResponse( return LnurlErrorResponse(reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,)
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
)
def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus: async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus:
payment = get_wallet_payment(wallet_id, payment_hash) payment = await get_wallet_payment(wallet_id, payment_hash)
if not payment: if not payment:
return PaymentStatus(None) return PaymentStatus(None)

View File

@ -2,7 +2,6 @@ import trio # type: ignore
import json import json
import lnurl # type: ignore import lnurl # type: ignore
import httpx import httpx
import traceback
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, jsonify, request, make_response from quart import g, jsonify, request, make_response
from http import HTTPStatus from http import HTTPStatus
@ -12,7 +11,7 @@ from typing import Dict, Union
from lnbits import bolt11 from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from .. import core_app from .. import core_app, db
from ..services import create_invoice, pay_invoice, perform_lnurlauth from ..services import create_invoice, pay_invoice, perform_lnurlauth
from ..crud import delete_expired_invoices from ..crud import delete_expired_invoices
from ..tasks import sse_listeners from ..tasks import sse_listeners
@ -22,13 +21,7 @@ from ..tasks import sse_listeners
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_wallet(): async def api_wallet():
return ( return (
jsonify( jsonify({"id": g.wallet.id, "name": g.wallet.name, "balance": g.wallet.balance_msat,}),
{
"id": g.wallet.id,
"name": g.wallet.name,
"balance": g.wallet.balance_msat,
}
),
HTTPStatus.OK, HTTPStatus.OK,
) )
@ -37,12 +30,12 @@ async def api_wallet():
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_payments(): async def api_payments():
if "check_pending" in request.args: if "check_pending" in request.args:
delete_expired_invoices() await delete_expired_invoices()
for payment in g.wallet.get_payments(complete=False, pending=True, exclude_uncheckable=True): for payment in await g.wallet.get_payments(complete=False, pending=True, exclude_uncheckable=True):
payment.check_pending() await payment.check_pending()
return jsonify(g.wallet.get_payments(pending=True)), HTTPStatus.OK return jsonify(await g.wallet.get_payments(pending=True)), HTTPStatus.OK
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
@ -63,12 +56,14 @@ async def api_payments_create_invoice():
memo = g.data["memo"] memo = g.data["memo"]
try: try:
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash
) )
except Exception as e: except Exception as exc:
g.db.rollback() await db.rollback()
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR raise exc
await db.commit()
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
@ -76,11 +71,7 @@ async def api_payments_create_invoice():
if g.data.get("lnurl_callback"): if g.data.get("lnurl_callback"):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
r = await client.get( r = await client.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10,)
g.data["lnurl_callback"],
params={"pr": payment_request},
timeout=10,
)
if r.is_error: if r.is_error:
lnurl_response = r.text lnurl_response = r.text
else: else:
@ -110,15 +101,14 @@ async def api_payments_create_invoice():
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
async def api_payments_pay_invoice(): async def api_payments_pay_invoice():
try: try:
payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"]) payment_hash = await pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"])
except ValueError as e: except ValueError as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
except PermissionError as e: except PermissionError as e:
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
except Exception as exc: except Exception as exc:
traceback.print_exc(7) await db.rollback()
g.db.rollback() raise exc
return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
return ( return (
jsonify( jsonify(
@ -157,9 +147,7 @@ async def api_payments_pay_lnurl():
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
r = await client.get( r = await client.get(
g.data["callback"], g.data["callback"], params={"amount": g.data["amount"], "comment": g.data["comment"]}, timeout=40,
params={"amount": g.data["amount"], "comment": g.data["comment"]},
timeout=40,
) )
if r.is_error: if r.is_error:
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
@ -198,16 +186,12 @@ async def api_payments_pay_lnurl():
if g.data["comment"]: if g.data["comment"]:
extra["comment"] = g.data["comment"] extra["comment"] = g.data["comment"]
payment_hash = pay_invoice( payment_hash = await pay_invoice(
wallet_id=g.wallet.id, wallet_id=g.wallet.id, payment_request=params["pr"], description=g.data.get("description", ""), extra=extra,
payment_request=params["pr"],
description=g.data.get("description", ""),
extra=extra,
) )
except Exception as exc: except Exception as exc:
traceback.print_exc(7) await db.rollback()
g.db.rollback() raise exc
return jsonify({"message": str(exc)}), HTTPStatus.INTERNAL_SERVER_ERROR
return ( return (
jsonify( jsonify(
@ -225,7 +209,7 @@ async def api_payments_pay_lnurl():
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"]) @core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_payment(payment_hash): async def api_payment(payment_hash):
payment = g.wallet.get_payment(payment_hash) payment = await g.wallet.get_payment(payment_hash)
if not payment: if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
@ -233,7 +217,7 @@ async def api_payment(payment_hash):
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
try: try:
payment.check_pending() await payment.check_pending()
except Exception: except Exception:
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
@ -243,7 +227,6 @@ async def api_payment(payment_hash):
@core_app.route("/api/v1/payments/sse", methods=["GET"]) @core_app.route("/api/v1/payments/sse", methods=["GET"])
@api_check_wallet_key("invoice", accept_querystring=True) @api_check_wallet_key("invoice", accept_querystring=True)
async def api_payments_sse(): async def api_payments_sse():
g.db.close()
this_wallet_id = g.wallet.id this_wallet_id = g.wallet.id
send_payment, receive_payment = trio.open_memory_channel(0) send_payment, receive_payment = trio.open_memory_channel(0)
@ -364,9 +347,7 @@ async def api_lnurlscan(code: str):
@core_app.route("/api/v1/lnurlauth", methods=["POST"]) @core_app.route("/api/v1/lnurlauth", methods=["POST"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
@api_validate_post_request( @api_validate_post_request(
schema={ schema={"callback": {"type": "string", "required": True},}
"callback": {"type": "string", "required": True},
}
) )
async def api_perform_lnurlauth(): async def api_perform_lnurlauth():
err = await perform_lnurlauth(g.data["callback"]) err = await perform_lnurlauth(g.data["callback"])

View File

@ -8,8 +8,8 @@ from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl
from lnbits.core import core_app from lnbits.core import core_app
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE
from lnbits.tasks import run_on_pseudo_request
from .. import db
from ..crud import ( from ..crud import (
create_account, create_account,
get_user, get_user,
@ -41,11 +41,11 @@ async def extensions():
abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.") abort(HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension.")
if extension_to_enable: if extension_to_enable:
update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1) await update_user_extension(user_id=g.user.id, extension=extension_to_enable, active=1)
elif extension_to_disable: elif extension_to_disable:
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0) await update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
return await render_template("core/extensions.html", user=get_user(g.user.id)) return await render_template("core/extensions.html", user=await get_user(g.user.id))
@core_app.route("/wallet") @core_app.route("/wallet")
@ -63,9 +63,12 @@ async def wallet():
# nothing: create everything # nothing: create everything
if not user_id: if not user_id:
user = get_user(create_account().id) user = await get_user((await create_account()).id)
else: else:
user = get_user(user_id) or abort(HTTPStatus.NOT_FOUND, "User does not exist.") user = await get_user(user_id)
if not user:
abort(HTTPStatus.NOT_FOUND, "User does not exist.")
return
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS: if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
@ -74,7 +77,7 @@ async def wallet():
if user.wallets and not wallet_name: if user.wallets and not wallet_name:
wallet = user.wallets[0] wallet = user.wallets[0]
else: else:
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
@ -95,7 +98,7 @@ async def deletewallet():
if wallet_id not in user_wallet_ids: if wallet_id not in user_wallet_ids:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.") abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
else: else:
delete_wallet(user_id=g.user.id, wallet_id=wallet_id) await delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id) user_wallet_ids.remove(wallet_id)
if user_wallet_ids: if user_wallet_ids:
@ -120,14 +123,12 @@ async def lnurlwallet():
except Exception as exc: except Exception as exc:
return f"Could not process lnurl-withdraw: {exc}", HTTPStatus.INTERNAL_SERVER_ERROR return f"Could not process lnurl-withdraw: {exc}", HTTPStatus.INTERNAL_SERVER_ERROR
account = create_account() account = await create_account()
user = get_user(account.id) user = await get_user(account.id)
wallet = create_wallet(user_id=user.id) wallet = await create_wallet(user_id=user.id)
g.db.commit() await db.commit()
await run_on_pseudo_request( g.nursery.start_soon(redeem_lnurl_withdraw, wallet.id, withdraw_res, "LNbits initial funding: voucher redeem.")
redeem_lnurl_withdraw, wallet.id, withdraw_res, "LNbits initial funding: voucher redeem."
)
await trio.sleep(3) await trio.sleep(3)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))

View File

@ -1,66 +1,85 @@
import os import os
import sqlite3 from typing import Tuple, Optional, Any
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore
from sqlalchemy import create_engine # type: ignore
from quart import g
from .settings import LNBITS_DATA_FOLDER from .settings import LNBITS_DATA_FOLDER
class Database: class Database:
def __init__(self, db_path: str): def __init__(self, db_name: str):
self.path = db_path self.db_name = db_name
self.connection = sqlite3.connect(db_path) db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3")
self.connection.row_factory = sqlite3.Row self.engine = create_engine(f"sqlite:///{db_path}", strategy=TRIO_STRATEGY)
self.cursor = self.connection.cursor()
self.closed = False
def close(self): def connect(self):
self.__exit__(None, None, None) return self.engine.connect()
def __enter__(self): def session_connection(self) -> Tuple[Optional[Any], Optional[Any]]:
return self try:
return getattr(g, f"{self.db_name}_conn", None), getattr(g, f"{self.db_name}_txn", None)
except RuntimeError:
return None, None
def __exit__(self, exc_type, exc_val, exc_tb): async def begin(self):
if self.closed: conn, _ = self.session_connection()
if conn:
return return
if exc_val: conn = await self.engine.connect()
self.connection.rollback() setattr(g, f"{self.db_name}_conn", conn)
self.cursor.close() txn = await conn.begin()
self.connection.close() setattr(g, f"{self.db_name}_txn", txn)
else:
self.connection.commit()
self.cursor.close()
self.connection.close()
self.closed = True async def fetchall(self, query: str, values: tuple = ()) -> list:
conn, _ = self.session_connection()
if conn:
result = await conn.execute(query, values)
return await result.fetchall()
def commit(self): async with self.connect() as conn:
self.connection.commit() result = await conn.execute(query, values)
return await result.fetchall()
def rollback(self): async def fetchone(self, query: str, values: tuple = ()):
self.connection.rollback() conn, _ = self.session_connection()
if conn:
result = await conn.execute(query, values)
row = await result.fetchone()
await result.close()
return row
def fetchall(self, query: str, values: tuple = ()) -> list: async with self.connect() as conn:
"""Given a query, return cursor.fetchall() rows.""" result = await conn.execute(query, values)
self.execute(query, values) row = await result.fetchone()
return self.cursor.fetchall() await result.close()
return row
def fetchone(self, query: str, values: tuple = ()): async def execute(self, query: str, values: tuple = ()):
self.execute(query, values) conn, _ = self.session_connection()
return self.cursor.fetchone() if conn:
return await conn.execute(query, values)
def execute(self, query: str, values: tuple = ()) -> None: async with self.connect() as conn:
"""Given a query, cursor.execute() it.""" return await conn.execute(query, values)
try:
self.cursor.execute(query, values)
except sqlite3.Error as exc:
self.connection.rollback()
raise exc
async def commit(self):
conn, txn = self.session_connection()
if conn and txn:
await txn.commit()
await self.close_session()
def open_db(db_name: str = "database") -> Database: async def rollback(self):
db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3") conn, txn = self.session_connection()
return Database(db_path=db_path) if conn and txn:
await txn.rollback()
await self.close_session()
async def close_session(self):
def open_ext_db(extension_name: str) -> Database: conn, txn = self.session_connection()
return open_db(f"ext_{extension_name}") if conn and txn:
await txn.close()
await conn.close()
delattr(g, f"{self.db_name}_conn")
delattr(g, f"{self.db_name}_txn")

View File

@ -15,7 +15,7 @@ def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
async def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
try: try:
key_value = request.headers.get("X-Api-Key") or request.args["api-key"] key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
g.wallet = get_wallet_for_key(key_value, key_type) g.wallet = await get_wallet_for_key(key_value, key_type)
except KeyError: except KeyError:
return ( return (
jsonify({"message": "`X-Api-Key` header missing."}), jsonify({"message": "`X-Api-Key` header missing."}),
@ -63,7 +63,9 @@ def check_user_exists(param: str = "usr"):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
async def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.") g.user = await get_user(request.args.get(param, type=str)) or abort(
HTTPStatus.NOT_FOUND, "User does not exist."
)
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS: if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_amilk")
amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates") amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates")

View File

@ -2,43 +2,39 @@ from base64 import urlsafe_b64encode
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db from . import db
from .models import AMilk from .models import AMilk
def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk: async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk:
with open_ext_db("amilk") as db: amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") await db.execute(
db.execute( """
""" INSERT INTO amilks (id, wallet, lnurl, atime, amount)
INSERT INTO amilks (id, wallet, lnurl, atime, amount) VALUES (?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?) """,
""", (amilk_id, wallet_id, lnurl, atime, amount),
(amilk_id, wallet_id, lnurl, atime, amount), )
)
return get_amilk(amilk_id) amilk = await get_amilk(amilk_id)
assert amilk, "Newly created amilk_id couldn't be retrieved"
return amilk
def get_amilk(amilk_id: str) -> Optional[AMilk]: async def get_amilk(amilk_id: str) -> Optional[AMilk]:
with open_ext_db("amilk") as db: row = await db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,))
row = db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,))
return AMilk(**row) if row else None return AMilk(**row) if row else None
def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]: async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("amilk") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,))
return [AMilk(**row) for row in rows] return [AMilk(**row) for row in rows]
def delete_amilk(amilk_id: str) -> None: async def delete_amilk(amilk_id: str) -> None:
with open_ext_db("amilk") as db: await db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,))
db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,))

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Initial amilks table. Initial amilks table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS amilks ( CREATE TABLE IF NOT EXISTS amilks (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,

View File

@ -2,8 +2,8 @@ from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.amilk import amilk_ext
from . import amilk_ext
from .crud import get_amilk from .crud import get_amilk
@ -16,5 +16,8 @@ async def index():
@amilk_ext.route("/<amilk_id>") @amilk_ext.route("/<amilk_id>")
async def wall(amilk_id): async def wall(amilk_id):
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.") amilk = await get_amilk(amilk_id)
if not amilk:
abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
return await render_template("amilk/wall.html", amilk=amilk) return await render_template("amilk/wall.html", amilk=amilk)

View File

@ -1,15 +1,15 @@
import httpx import httpx
from quart import g, jsonify, request, abort from quart import g, jsonify, request, abort
from http import HTTPStatus from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
from lnurl.exceptions import LnurlException from lnurl.exceptions import LnurlException # type: ignore
from time import sleep from time import sleep
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.extensions.amilk import amilk_ext from . import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
@ -19,14 +19,14 @@ async def api_amilks():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([amilk._asdict() for amilk in get_amilks(wallet_ids)]), HTTPStatus.OK return jsonify([amilk._asdict() for amilk in await get_amilks(wallet_ids)]), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"]) @amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
async def api_amilkit(amilk_id): async def api_amilkit(amilk_id):
milk = get_amilk(amilk_id) milk = await get_amilk(amilk_id)
memo = milk.id memo = milk.id
try: try:
@ -34,7 +34,7 @@ async def api_amilkit(amilk_id):
except LnurlException: except LnurlException:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"} wallet_id=milk.wallet, amount=withdraw_res.max_sats, memo=memo, extra={"tag": "amilk"}
) )
@ -48,7 +48,7 @@ async def api_amilkit(amilk_id):
for i in range(10): for i in range(10):
sleep(i) sleep(i)
invoice_status = check_invoice_status(milk.wallet, payment_hash) invoice_status = await check_invoice_status(milk.wallet, payment_hash)
if invoice_status.paid: if invoice_status.paid:
return jsonify({"paid": True}), HTTPStatus.OK return jsonify({"paid": True}), HTTPStatus.OK
else: else:
@ -67,7 +67,9 @@ async def api_amilkit(amilk_id):
} }
) )
async def api_amilk_create(): async def api_amilk_create():
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"]) amilk = await create_amilk(
wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"]
)
return jsonify(amilk._asdict()), HTTPStatus.CREATED return jsonify(amilk._asdict()), HTTPStatus.CREATED
@ -75,7 +77,7 @@ async def api_amilk_create():
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"]) @amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_amilk_delete(amilk_id): async def api_amilk_delete(amilk_id):
amilk = get_amilk(amilk_id) amilk = await get_amilk(amilk_id)
if not amilk: if not amilk:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
@ -83,6 +85,6 @@ async def api_amilk_delete(amilk_id):
if amilk.wallet != g.wallet.id: if amilk.wallet != g.wallet.id:
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
delete_amilk(amilk_id) await delete_amilk(amilk_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Initial products table. Initial products table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS products ( CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -20,7 +20,7 @@ def m001_initial(db):
""" """
Initial indexers table. Initial indexers table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS indexers ( CREATE TABLE IF NOT EXISTS indexers (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -41,7 +41,7 @@ def m001_initial(db):
""" """
Initial orders table. Initial orders table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS orders ( CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,

View File

@ -1,4 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_events")
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates") events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")

View File

@ -1,45 +1,46 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Tickets, Events from .models import Tickets, Events
#######TICKETS######## # TICKETS
def create_ticket(payment_hash: str, wallet: str, event: str, name: str, email: str) -> Tickets: async def create_ticket(payment_hash: str, wallet: str, event: str, name: str, email: str) -> Tickets:
with open_ext_db("events") as db: await db.execute(
db.execute( """
""" INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
INSERT INTO ticket (id, wallet, event, name, email, registered, paid) VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?) """,
""", (payment_hash, wallet, event, name, email, False, False),
(payment_hash, wallet, event, name, email, False, False), )
)
return get_ticket(payment_hash) ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
return ticket
def update_ticket(paid: bool, payment_hash: str) -> Tickets: async def set_ticket_paid(payment_hash: str) -> Tickets:
with open_ext_db("events") as db: row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,)) if row[6] != True:
if row[6] == True: await db.execute(
return get_ticket(payment_hash)
db.execute(
""" """
UPDATE ticket UPDATE ticket
SET paid = ? SET paid = true
WHERE id = ? WHERE id = ?
""", """,
(paid, payment_hash), (payment_hash,),
) )
eventdata = get_event(row[2]) eventdata = await get_event(row[2])
assert eventdata, "Couldn't get event from ticket being paid"
sold = eventdata.sold + 1 sold = eventdata.sold + 1
amount_tickets = eventdata.amount_tickets - 1 amount_tickets = eventdata.amount_tickets - 1
db.execute( await db.execute(
""" """
UPDATE events UPDATE events
SET sold = ?, amount_tickets = ? SET sold = ?, amount_tickets = ?
@ -47,36 +48,34 @@ def update_ticket(paid: bool, payment_hash: str) -> Tickets:
""", """,
(sold, amount_tickets, row[2]), (sold, amount_tickets, row[2]),
) )
return get_ticket(payment_hash)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly updated ticket couldn't be retrieved"
return ticket
def get_ticket(payment_hash: str) -> Optional[Tickets]: async def get_ticket(payment_hash: str) -> Optional[Tickets]:
with open_ext_db("events") as db: row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
return Tickets(**row) if row else None return Tickets(**row) if row else None
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("events") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
print("scrum")
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]
def delete_ticket(payment_hash: str) -> None: async def delete_ticket(payment_hash: str) -> None:
with open_ext_db("events") as db: await db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,))
db.execute("DELETE FROM ticket WHERE id = ?", (payment_hash,))
########EVENTS######### # EVENTS
def create_event( async def create_event(
*, *,
wallet: str, wallet: str,
name: str, name: str,
@ -87,81 +86,68 @@ def create_event(
amount_tickets: int, amount_tickets: int,
price_per_ticket: int, price_per_ticket: int,
) -> Events: ) -> Events:
with open_ext_db("events") as db: event_id = urlsafe_short_hash()
event_id = urlsafe_short_hash() await db.execute(
db.execute( """
""" INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """,
""", (
( event_id,
event_id, wallet,
wallet, name,
name, info,
info, closing_date,
closing_date, event_start_date,
event_start_date, event_end_date,
event_end_date, amount_tickets,
amount_tickets, price_per_ticket,
price_per_ticket, 0,
0, ),
), )
)
print(event_id)
return get_event(event_id) event = await get_event(event_id)
assert event, "Newly created event couldn't be retrieved"
return event
def update_event(event_id: str, **kwargs) -> Events: async def update_event(event_id: str, **kwargs) -> Events:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("events") as db: await db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id)) event = await get_event(event_id)
assert event, "Newly updated event couldn't be retrieved"
return event
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
async def get_event(event_id: str) -> Optional[Events]:
row = await db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None return Events(**row) if row else None
def get_event(event_id: str) -> Optional[Events]: async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
with open_ext_db("events") as db:
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
return Events(**row) if row else None
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("events") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
return [Events(**row) for row in rows] return [Events(**row) for row in rows]
def delete_event(event_id: str) -> None: async def delete_event(event_id: str) -> None:
with open_ext_db("events") as db: await db.execute("DELETE FROM events WHERE id = ?", (event_id,))
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
########EVENTTICKETS######### # EVENTTICKETS
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets: async def get_event_tickets(event_id: str, wallet_id: str) -> List[Tickets]:
rows = await db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
with open_ext_db("events") as db:
rows = db.fetchall("SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id))
print(rows)
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]
def reg_ticket(ticket_id: str) -> Tickets: async def reg_ticket(ticket_id: str) -> List[Tickets]:
with open_ext_db("events") as db: await db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id))
db.execute("UPDATE ticket SET registered = ? WHERE id = ?", (True, ticket_id)) ticket = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
ticket = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,)) rows = await db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
print(ticket[1])
rows = db.fetchall("SELECT * FROM ticket WHERE event = ?", (ticket[1],))
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]

View File

@ -1,6 +1,6 @@
def m001_initial(db): async def m001_initial(db):
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS events ( CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -18,7 +18,7 @@ def m001_initial(db):
""" """
) )
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -33,9 +33,9 @@ def m001_initial(db):
) )
def m002_changed(db): async def m002_changed(db):
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS ticket ( CREATE TABLE IF NOT EXISTS ticket (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -50,7 +50,7 @@ def m002_changed(db):
""" """
) )
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]: for row in [list(row) for row in await db.fetchall("SELECT * FROM tickets")]:
usescsv = "" usescsv = ""
for i in range(row[5]): for i in range(row[5]):
@ -59,7 +59,7 @@ def m002_changed(db):
else: else:
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
db.execute( await db.execute(
""" """
INSERT INTO ticket ( INSERT INTO ticket (
id, id,
@ -72,14 +72,6 @@ def m002_changed(db):
) )
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (row[0], row[1], row[2], row[3], row[4], row[5], True,),
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
True,
),
) )
db.execute("DROP TABLE tickets") await db.execute("DROP TABLE tickets")

View File

@ -1,10 +1,10 @@
from quart import g, abort, render_template from quart import g, abort, render_template
from datetime import date, datetime from datetime import date, datetime
from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus from http import HTTPStatus
from lnbits.extensions.events import events_ext from lnbits.decorators import check_user_exists, validate_uuids
from . import events_ext
from .crud import get_ticket, get_event from .crud import get_ticket, get_event
@ -17,7 +17,10 @@ async def index():
@events_ext.route("/<event_id>") @events_ext.route("/<event_id>")
async def display(event_id): async def display(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") event = await get_event(event_id)
if not event:
abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
if event.amount_tickets < 1: if event.amount_tickets < 1:
return await render_template( return await render_template(
"events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :(" "events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :("
@ -39,8 +42,14 @@ async def display(event_id):
@events_ext.route("/ticket/<ticket_id>") @events_ext.route("/ticket/<ticket_id>")
async def ticket(ticket_id): async def ticket(ticket_id):
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.") ticket = await get_ticket(ticket_id)
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") if not ticket:
abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
event = await get_event(ticket.event)
if not event:
abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return await render_template( return await render_template(
"events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info "events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info
) )
@ -48,7 +57,9 @@ async def ticket(ticket_id):
@events_ext.route("/register/<event_id>") @events_ext.route("/register/<event_id>")
async def register(event_id): async def register(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") event = await get_event(event_id)
if not event:
abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return await render_template( return await render_template(
"events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet "events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet

View File

@ -5,10 +5,10 @@ from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.events import events_ext from . import events_ext
from .crud import ( from .crud import (
create_ticket, create_ticket,
update_ticket, set_ticket_paid,
get_ticket, get_ticket,
get_tickets, get_tickets,
delete_ticket, delete_ticket,
@ -22,7 +22,7 @@ from .crud import (
) )
#########Events########## # Events
@events_ext.route("/api/v1/events", methods=["GET"]) @events_ext.route("/api/v1/events", methods=["GET"])
@ -31,9 +31,9 @@ async def api_events():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([event._asdict() for event in get_events(wallet_ids)]), HTTPStatus.OK return jsonify([event._asdict() for event in await get_events(wallet_ids)]), HTTPStatus.OK
@events_ext.route("/api/v1/events", methods=["POST"]) @events_ext.route("/api/v1/events", methods=["POST"])
@ -53,35 +53,31 @@ async def api_events():
) )
async def api_event_create(event_id=None): async def api_event_create(event_id=None):
if event_id: if event_id:
event = get_event(event_id) event = await get_event(event_id)
print(g.data)
if not event: if not event:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id: if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
event = update_event(event_id, **g.data) event = await update_event(event_id, **g.data)
else: else:
event = create_event(**g.data) event = await create_event(**g.data)
print(event)
return jsonify(event._asdict()), HTTPStatus.CREATED return jsonify(event._asdict()), HTTPStatus.CREATED
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"]) @events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_form_delete(event_id): async def api_form_delete(event_id):
event = get_event(event_id) event = await get_event(event_id)
if not event: if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id: if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
delete_event(event_id) await delete_event(event_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -94,9 +90,9 @@ async def api_tickets():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([ticket._asdict() for ticket in get_tickets(wallet_ids)]), HTTPStatus.OK return jsonify([ticket._asdict() for ticket in await get_tickets(wallet_ids)]), HTTPStatus.OK
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"]) @events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
@ -107,17 +103,17 @@ async def api_tickets():
} }
) )
async def api_ticket_make_ticket(event_id, sats): async def api_ticket_make_ticket(event_id, sats):
event = get_event(event_id) event = await get_event(event_id)
if not event: if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
try: try:
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet, amount=int(sats), memo=f"{event_id}", extra={"tag": "events"} wallet_id=event.wallet, amount=int(sats), memo=f"{event_id}", extra={"tag": "events"}
) )
except Exception as e: except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data) ticket = await create_ticket(payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND
@ -127,17 +123,19 @@ async def api_ticket_make_ticket(event_id, sats):
@events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
async def api_ticket_send_ticket(payment_hash): async def api_ticket_send_ticket(payment_hash):
ticket = get_ticket(payment_hash) ticket = await get_ticket(payment_hash)
try: try:
is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending status = await check_invoice_status(ticket.wallet, payment_hash)
is_paid = not status.pending
except Exception: except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid: if is_paid:
wallet = get_wallet(ticket.wallet) wallet = await get_wallet(ticket.wallet)
payment = wallet.get_payment(payment_hash) payment = await wallet.get_payment(payment_hash)
payment.set_pending(False) await payment.set_pending(False)
ticket = update_ticket(paid=True, payment_hash=payment_hash) ticket = await set_ticket_paid(payment_hash=payment_hash)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
@ -147,7 +145,7 @@ async def api_ticket_send_ticket(payment_hash):
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"]) @events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_ticket_delete(ticket_id): async def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id) ticket = await get_ticket(ticket_id)
if not ticket: if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
@ -155,32 +153,28 @@ async def api_ticket_delete(ticket_id):
if ticket.wallet != g.wallet.id: if ticket.wallet != g.wallet.id:
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
delete_ticket(ticket_id) await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
#########EventTickets########## # Event Tickets
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"]) @events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
async def api_event_tickets(wallet_id, event_id): async def api_event_tickets(wallet_id, event_id):
return ( return (
jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), jsonify([ticket._asdict() for ticket in await get_event_tickets(wallet_id=wallet_id, event_id=event_id)]),
HTTPStatus.OK, HTTPStatus.OK,
) )
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"]) @events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
async def api_event_register_ticket(ticket_id): async def api_event_register_ticket(ticket_id):
ticket = await get_ticket(ticket_id)
ticket = get_ticket(ticket_id)
if not ticket: if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
if ticket.registered == True: if ticket.registered == True:
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
return jsonify([ticket._asdict() for ticket in reg_ticket(ticket_id)]), HTTPStatus.OK return jsonify([ticket._asdict() for ticket in await reg_ticket(ticket_id)]), HTTPStatus.OK

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_example")
example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates") example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates")

View File

@ -1,7 +1,8 @@
from quart import g, render_template from quart import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.example import example_ext
from . import example_ext
@example_ext.route("/") @example_ext.route("/")

View File

@ -10,7 +10,7 @@
from quart import jsonify from quart import jsonify
from http import HTTPStatus from http import HTTPStatus
from lnbits.extensions.example import example_ext from . import example_ext
# add your endpoints here # add your endpoints here
@ -20,21 +20,9 @@ from lnbits.extensions.example import example_ext
async def api_example(): async def api_example():
"""Try to add descriptions for others.""" """Try to add descriptions for others."""
tools = [ tools = [
{ {"name": "Flask", "url": "https://flask.palletsprojects.com/", "language": "Python",},
"name": "Flask", {"name": "Vue.js", "url": "https://vuejs.org/", "language": "JavaScript",},
"url": "https://flask.palletsprojects.com/", {"name": "Quasar Framework", "url": "https://quasar.dev/", "language": "JavaScript",},
"language": "Python",
},
{
"name": "Vue.js",
"url": "https://vuejs.org/",
"language": "JavaScript",
},
{
"name": "Quasar Framework",
"url": "https://quasar.dev/",
"language": "JavaScript",
},
] ]
return jsonify(tools), HTTPStatus.OK return jsonify(tools), HTTPStatus.OK

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_lndhub")
lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates") lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates")

View File

@ -15,7 +15,7 @@ def check_wallet(requires_admin=False):
if requires_admin and key_type != "admin": if requires_admin and key_type != "admin":
return jsonify({"error": True, "code": 2, "message": "insufficient permissions"}) return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
g.wallet = get_wallet_for_key(key, key_type) g.wallet = await get_wallet_for_key(key, key_type)
if not g.wallet: if not g.wallet:
return jsonify({"error": True, "code": 2, "message": "insufficient permissions"}) return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
return await view(**kwargs) return await view(**kwargs)

View File

@ -1,2 +1,2 @@
def migrate(): async def migrate():
pass pass

View File

@ -1,7 +1,7 @@
from quart import render_template, g from quart import render_template, g
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.lndhub import lndhub_ext from . import lndhub_ext
@lndhub_ext.route("/") @lndhub_ext.route("/")

View File

@ -8,7 +8,7 @@ from lnbits.decorators import api_validate_post_request
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits import bolt11 from lnbits import bolt11
from lnbits.extensions.lndhub import lndhub_ext from . import lndhub_ext
from .decorators import check_wallet from .decorators import check_wallet
from .utils import to_buffer, decoded_as_lndhub from .utils import to_buffer, decoded_as_lndhub
@ -46,20 +46,11 @@ async def lndhub_auth():
) )
async def lndhub_addinvoice(): async def lndhub_addinvoice():
try: try:
_, pr = create_invoice( _, pr = await create_invoice(
wallet_id=g.wallet.id, wallet_id=g.wallet.id, amount=int(g.data["amt"]), memo=g.data["memo"], extra={"tag": "lndhub"},
amount=int(g.data["amt"]),
memo=g.data["memo"],
extra={"tag": "lndhub"},
) )
except Exception as e: except Exception as e:
return jsonify( return jsonify({"error": True, "code": 7, "message": "Failed to create invoice: " + str(e),})
{
"error": True,
"code": 7,
"message": "Failed to create invoice: " + str(e),
}
)
invoice = bolt11.decode(pr) invoice = bolt11.decode(pr)
return jsonify( return jsonify(
@ -78,19 +69,11 @@ async def lndhub_addinvoice():
@api_validate_post_request(schema={"invoice": {"type": "string", "required": True}}) @api_validate_post_request(schema={"invoice": {"type": "string", "required": True}})
async def lndhub_payinvoice(): async def lndhub_payinvoice():
try: try:
pay_invoice( await pay_invoice(
wallet_id=g.wallet.id, wallet_id=g.wallet.id, payment_request=g.data["invoice"], extra={"tag": "lndhub"},
payment_request=g.data["invoice"],
extra={"tag": "lndhub"},
) )
except Exception as e: except Exception as e:
return jsonify( return jsonify({"error": True, "code": 10, "message": "Payment failed: " + str(e),})
{
"error": True,
"code": 10,
"message": "Payment failed: " + str(e),
}
)
invoice: bolt11.Invoice = bolt11.decode(g.data["invoice"]) invoice: bolt11.Invoice = bolt11.decode(g.data["invoice"])
return jsonify( return jsonify(
@ -119,10 +102,10 @@ async def lndhub_balance():
@lndhub_ext.route("/ext/gettxs", methods=["GET"]) @lndhub_ext.route("/ext/gettxs", methods=["GET"])
@check_wallet() @check_wallet()
async def lndhub_gettxs(): async def lndhub_gettxs():
for payment in g.wallet.get_payments( for payment in await g.wallet.get_payments(
complete=False, pending=True, outgoing=True, incoming=False, exclude_uncheckable=True complete=False, pending=True, outgoing=True, incoming=False, exclude_uncheckable=True
): ):
payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending) await payment.set_pending(WALLET.get_payment_status(payment.checking_id).pending)
limit = int(request.args.get("limit", 200)) limit = int(request.args.get("limit", 200))
return jsonify( return jsonify(
@ -138,7 +121,7 @@ async def lndhub_gettxs():
"memo": payment.memo if not payment.pending else "Payment in transition", "memo": payment.memo if not payment.pending else "Payment in transition",
} }
for payment in reversed( for payment in reversed(
g.wallet.get_payments(pending=True, complete=True, outgoing=True, incoming=False)[:limit] (await g.wallet.get_payments(pending=True, complete=True, outgoing=True, incoming=False))[:limit]
) )
] ]
) )
@ -147,11 +130,11 @@ async def lndhub_gettxs():
@lndhub_ext.route("/ext/getuserinvoices", methods=["GET"]) @lndhub_ext.route("/ext/getuserinvoices", methods=["GET"])
@check_wallet() @check_wallet()
async def lndhub_getuserinvoices(): async def lndhub_getuserinvoices():
delete_expired_invoices() await delete_expired_invoices()
for invoice in g.wallet.get_payments( for invoice in await g.wallet.get_payments(
complete=False, pending=True, outgoing=False, incoming=True, exclude_uncheckable=True complete=False, pending=True, outgoing=False, incoming=True, exclude_uncheckable=True
): ):
invoice.set_pending(WALLET.get_invoice_status(invoice.checking_id).pending) await invoice.set_pending(WALLET.get_invoice_status(invoice.checking_id).pending)
limit = int(request.args.get("limit", 200)) limit = int(request.args.get("limit", 200))
return jsonify( return jsonify(
@ -169,7 +152,7 @@ async def lndhub_getuserinvoices():
"type": "user_invoice", "type": "user_invoice",
} }
for invoice in reversed( for invoice in reversed(
g.wallet.get_payments(pending=True, complete=True, incoming=True, outgoing=False)[:limit] (await g.wallet.get_payments(pending=True, complete=True, incoming=True, outgoing=False))[:limit]
) )
] ]
) )

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_lnticket")
lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates") lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates")

View File

@ -1,44 +1,44 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Tickets, Forms from .models import Tickets, Forms
#######TICKETS######## async def create_ticket(
payment_hash: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int,
) -> Tickets:
await db.execute(
"""
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, form, email, ltext, name, wallet, sats, False),
)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly created ticket couldn't be retrieved"
return ticket
def create_ticket(payment_hash: str, wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets: async def set_ticket_paid(payment_hash: str) -> Tickets:
with open_ext_db("lnticket") as db: row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
db.execute( if row[7] == False:
""" await db.execute(
INSERT INTO ticket (id, form, email, ltext, name, wallet, sats, paid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, form, email, ltext, name, wallet, sats, False),
)
return get_ticket(payment_hash)
def update_ticket(paid: bool, payment_hash: str) -> Tickets:
with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (payment_hash,))
if row[7] == True:
return get_ticket(payment_hash)
db.execute(
""" """
UPDATE ticket UPDATE ticket
SET paid = ? SET paid = true
WHERE id = ? WHERE id = ?
""", """,
(paid, payment_hash), (payment_hash,),
) )
formdata = get_form(row[1]) formdata = await get_form(row[1])
assert formdata, "Couldn't get form from paid ticket"
amount = formdata.amountmade + row[7] amount = formdata.amountmade + row[7]
db.execute( await db.execute(
""" """
UPDATE forms UPDATE forms
SET amountmade = ? SET amountmade = ?
@ -46,76 +46,71 @@ def update_ticket(paid: bool, payment_hash: str) -> Tickets:
""", """,
(amount, row[1]), (amount, row[1]),
) )
return get_ticket(payment_hash)
ticket = await get_ticket(payment_hash)
assert ticket, "Newly updated ticket couldn't be retrieved"
return ticket
def get_ticket(ticket_id: str) -> Optional[Tickets]: async def get_ticket(ticket_id: str) -> Optional[Tickets]:
with open_ext_db("lnticket") as db: row = await db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
row = db.fetchone("SELECT * FROM ticket WHERE id = ?", (ticket_id,))
return Tickets(**row) if row else None return Tickets(**row) if row else None
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("lnticket") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM ticket WHERE wallet IN ({q})", (*wallet_ids,))
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]
def delete_ticket(ticket_id: str) -> None: async def delete_ticket(ticket_id: str) -> None:
with open_ext_db("lnticket") as db: await db.execute("DELETE FROM ticket WHERE id = ?", (ticket_id,))
db.execute("DELETE FROM ticket WHERE id = ?", (ticket_id,))
########FORMS######### # FORMS
def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms: async def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms:
with open_ext_db("lnticket") as db: form_id = urlsafe_short_hash()
form_id = urlsafe_short_hash() await db.execute(
db.execute( """
""" INSERT INTO forms (id, wallet, name, description, costpword, amountmade)
INSERT INTO forms (id, wallet, name, description, costpword, amountmade) VALUES (?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?) """,
""", (form_id, wallet, name, description, costpword, 0),
(form_id, wallet, name, description, costpword, 0), )
)
return get_form(form_id) form = await get_form(form_id)
assert form, "Newly created form couldn't be retrieved"
return form
def update_form(form_id: str, **kwargs) -> Forms: async def update_form(form_id: str, **kwargs) -> Forms:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("lnticket") as db: await db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id))
db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id)) row = await db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,)) assert row, "Newly updated form couldn't be retrieved"
return Forms(**row)
async def get_form(form_id: str) -> Optional[Forms]:
row = await db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
return Forms(**row) if row else None return Forms(**row) if row else None
def get_form(form_id: str) -> Optional[Forms]: async def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
with open_ext_db("lnticket") as db:
row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,))
return Forms(**row) if row else None
def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("lnticket") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,))
return [Forms(**row) for row in rows] return [Forms(**row) for row in rows]
def delete_form(form_id: str) -> None: async def delete_form(form_id: str) -> None:
with open_ext_db("lnticket") as db: await db.execute("DELETE FROM forms WHERE id = ?", (form_id,))
db.execute("DELETE FROM forms WHERE id = ?", (form_id,))

View File

@ -1,6 +1,6 @@
def m001_initial(db): async def m001_initial(db):
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS forms ( CREATE TABLE IF NOT EXISTS forms (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -14,7 +14,7 @@ def m001_initial(db):
""" """
) )
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -30,9 +30,9 @@ def m001_initial(db):
) )
def m002_changed(db): async def m002_changed(db):
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS ticket ( CREATE TABLE IF NOT EXISTS ticket (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -48,7 +48,7 @@ def m002_changed(db):
""" """
) )
for row in [list(row) for row in db.fetchall("SELECT * FROM tickets")]: for row in [list(row) for row in await db.fetchall("SELECT * FROM tickets")]:
usescsv = "" usescsv = ""
for i in range(row[5]): for i in range(row[5]):
@ -57,7 +57,7 @@ def m002_changed(db):
else: else:
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
db.execute( await db.execute(
""" """
INSERT INTO ticket ( INSERT INTO ticket (
id, id,
@ -71,15 +71,6 @@ def m002_changed(db):
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (row[0], row[1], row[2], row[3], row[4], row[5], row[6], True,),
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
row[6],
True,
),
) )
db.execute("DROP TABLE tickets") await db.execute("DROP TABLE tickets")

View File

@ -3,7 +3,7 @@ from quart import g, abort, render_template
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus from http import HTTPStatus
from lnbits.extensions.lnticket import lnticket_ext from . import lnticket_ext
from .crud import get_form from .crud import get_form
@ -16,8 +16,9 @@ async def index():
@lnticket_ext.route("/<form_id>") @lnticket_ext.route("/<form_id>")
async def display(form_id): async def display(form_id):
form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.") form = await get_form(form_id)
print(form.id) if not form:
abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
return await render_template( return await render_template(
"lnticket/display.html", "lnticket/display.html",

View File

@ -6,10 +6,10 @@ from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.lnticket import lnticket_ext from . import lnticket_ext
from .crud import ( from .crud import (
create_ticket, create_ticket,
update_ticket, set_ticket_paid,
get_ticket, get_ticket,
get_tickets, get_tickets,
delete_ticket, delete_ticket,
@ -21,7 +21,7 @@ from .crud import (
) )
#########FORMS########## # FORMS
@lnticket_ext.route("/api/v1/forms", methods=["GET"]) @lnticket_ext.route("/api/v1/forms", methods=["GET"])
@ -30,9 +30,9 @@ async def api_forms():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([form._asdict() for form in get_forms(wallet_ids)]), HTTPStatus.OK return jsonify([form._asdict() for form in await get_forms(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/forms", methods=["POST"]) @lnticket_ext.route("/api/v1/forms", methods=["POST"])
@ -48,7 +48,7 @@ async def api_forms():
) )
async def api_form_create(form_id=None): async def api_form_create(form_id=None):
if form_id: if form_id:
form = get_form(form_id) form = await get_form(form_id)
if not form: if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
@ -56,16 +56,16 @@ async def api_form_create(form_id=None):
if form.wallet != g.wallet.id: if form.wallet != g.wallet.id:
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
form = update_form(form_id, **g.data) form = await update_form(form_id, **g.data)
else: else:
form = create_form(**g.data) form = await create_form(**g.data)
return jsonify(form._asdict()), HTTPStatus.CREATED return jsonify(form._asdict()), HTTPStatus.CREATED
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"]) @lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_form_delete(form_id): async def api_form_delete(form_id):
form = get_form(form_id) form = await get_form(form_id)
if not form: if not form:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
@ -73,7 +73,7 @@ async def api_form_delete(form_id):
if form.wallet != g.wallet.id: if form.wallet != g.wallet.id:
return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN
delete_form(form_id) await delete_form(form_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -87,9 +87,9 @@ async def api_tickets():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK return jsonify([form._asdict() for form in await get_tickets(wallet_ids)]), HTTPStatus.OK
@lnticket_ext.route("/api/v1/tickets/<form_id>", methods=["POST"]) @lnticket_ext.route("/api/v1/tickets/<form_id>", methods=["POST"])
@ -102,22 +102,17 @@ async def api_tickets():
} }
) )
async def api_ticket_make_ticket(form_id): async def api_ticket_make_ticket(form_id):
form = get_form(form_id) form = await get_form(form_id)
if not form: if not form:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
try:
nwords = len(re.split(r"\s+", g.data["ltext"]))
sats = nwords * form.costpword
payment_hash, payment_request = create_invoice(
wallet_id=form.wallet,
amount=sats,
memo=f"ticket with {nwords} words on {form_id}",
extra={"tag": "lnticket"},
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = create_ticket(payment_hash=payment_hash, wallet=form.wallet, sats=sats, **g.data) nwords = len(re.split(r"\s+", g.data["ltext"]))
sats = nwords * form.costpword
payment_hash, payment_request = await create_invoice(
wallet_id=form.wallet, amount=sats, memo=f"ticket with {nwords} words on {form_id}", extra={"tag": "lnticket"},
)
ticket = await create_ticket(payment_hash=payment_hash, wallet=form.wallet, sats=sats, **g.data)
if not ticket: if not ticket:
return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket could not be fetched."}), HTTPStatus.NOT_FOUND
@ -127,17 +122,18 @@ async def api_ticket_make_ticket(form_id):
@lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
async def api_ticket_send_ticket(payment_hash): async def api_ticket_send_ticket(payment_hash):
ticket = get_ticket(payment_hash) ticket = await get_ticket(payment_hash)
try: try:
is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending status = await check_invoice_status(ticket.wallet, payment_hash)
is_paid = not status.pending
except Exception: except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid: if is_paid:
wallet = get_wallet(ticket.wallet) wallet = await get_wallet(ticket.wallet)
payment = wallet.get_payment(payment_hash) payment = await wallet.get_payment(payment_hash)
payment.set_pending(False) await payment.set_pending(False)
ticket = update_ticket(paid=True, payment_hash=payment_hash) ticket = await set_ticket_paid(payment_hash=payment_hash)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
@ -146,7 +142,7 @@ async def api_ticket_send_ticket(payment_hash):
@lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"]) @lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_ticket_delete(ticket_id): async def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id) ticket = await get_ticket(ticket_id)
if not ticket: if not ticket:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
@ -154,6 +150,6 @@ async def api_ticket_delete(ticket_id):
if ticket.wallet != g.wallet.id: if ticket.wallet != g.wallet.id:
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
delete_ticket(ticket_id) await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_lnurlp")
lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates") lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates")

View File

@ -1,14 +1,10 @@
import json
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db from . import db
from lnbits.core.models import Payment
from quart import g
from .models import PayLink from .models import PayLink
def create_pay_link( async def create_pay_link(
*, *,
wallet_id: str, wallet_id: str,
description: str, description: str,
@ -19,96 +15,66 @@ def create_pay_link(
webhook_url: Optional[str] = None, webhook_url: Optional[str] = None,
success_text: Optional[str] = None, success_text: Optional[str] = None,
success_url: Optional[str] = None, success_url: Optional[str] = None,
) -> Optional[PayLink]: ) -> PayLink:
with open_ext_db("lnurlp") as db: result = await db.execute(
db.execute( """
""" INSERT INTO pay_links (
INSERT INTO pay_links ( wallet,
wallet, description,
description, min,
min, max,
max, served_meta,
served_meta, served_pr,
served_pr, webhook_url,
webhook_url, success_text,
success_text, success_url,
success_url, comment_chars,
comment_chars, currency
currency
)
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
""",
(
wallet_id,
description,
min,
max,
webhook_url,
success_text,
success_url,
comment_chars,
currency,
),
) )
link_id = db.cursor.lastrowid VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
return get_pay_link(link_id) """,
(wallet_id, description, min, max, webhook_url, success_text, success_url, comment_chars, currency,),
)
link_id = result._result_proxy.lastrowid
link = await get_pay_link(link_id)
assert link, "Newly created link couldn't be retrieved"
return link
def get_pay_link(link_id: int) -> Optional[PayLink]: async def get_pay_link(link_id: int) -> Optional[PayLink]:
with open_ext_db("lnurlp") as db: row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None return PayLink.from_row(row) if row else None
def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]: async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("lnurlp") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(
rows = db.fetchall( f"""
f""" SELECT * FROM pay_links WHERE wallet IN ({q})
SELECT * FROM pay_links WHERE wallet IN ({q}) ORDER BY Id
ORDER BY Id """,
""", (*wallet_ids,),
(*wallet_ids,), )
)
return [PayLink.from_row(row) for row in rows] return [PayLink.from_row(row) for row in rows]
def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: async def update_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
with open_ext_db("lnurlp") as db: row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None return PayLink.from_row(row) if row else None
def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]: async def increment_pay_link(link_id: int, **kwargs) -> Optional[PayLink]:
q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()])
await db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
with open_ext_db("lnurlp") as db: row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
db.execute(f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id))
row = db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None return PayLink.from_row(row) if row else None
def delete_pay_link(link_id: int) -> None: async def delete_pay_link(link_id: int) -> None:
with open_ext_db("lnurlp") as db: await db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
db.execute("DELETE FROM pay_links WHERE id = ?", (link_id,))
def mark_webhook_sent(payment: Payment, status: int) -> None:
payment.extra["wh_status"] = status
g.db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)

View File

@ -13,7 +13,7 @@ from .helpers import get_fiat_rate
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
async def api_lnurl_response(link_id): async def api_lnurl_response(link_id):
link = increment_pay_link(link_id, served_meta=1) link = await increment_pay_link(link_id, served_meta=1)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
@ -34,7 +34,7 @@ async def api_lnurl_response(link_id):
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
async def api_lnurl_callback(link_id): async def api_lnurl_callback(link_id):
link = increment_pay_link(link_id, served_pr=1) link = await increment_pay_link(link_id, served_pr=1)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
@ -71,7 +71,7 @@ async def api_lnurl_callback(link_id):
HTTPStatus.OK, HTTPStatus.OK,
) )
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=link.wallet, wallet_id=link.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=link.description, memo=link.description,
@ -79,10 +79,6 @@ async def api_lnurl_callback(link_id):
extra={"tag": "lnurlp", "link": link.id, "comment": comment}, extra={"tag": "lnurlp", "link": link.id, "comment": comment},
) )
resp = LnurlPayActionResponse( resp = LnurlPayActionResponse(pr=payment_request, success_action=link.success_action(payment_hash), routes=[],)
pr=payment_request,
success_action=link.success_action(payment_hash),
routes=[],
)
return jsonify(resp.dict()), HTTPStatus.OK return jsonify(resp.dict()), HTTPStatus.OK

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Initial pay table. Initial pay table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS pay_links ( CREATE TABLE IF NOT EXISTS pay_links (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -16,14 +16,14 @@ def m001_initial(db):
) )
def m002_webhooks_and_success_actions(db): async def m002_webhooks_and_success_actions(db):
""" """
Webhooks and success actions. Webhooks and success actions.
""" """
db.execute("ALTER TABLE pay_links ADD COLUMN webhook_url TEXT;") await db.execute("ALTER TABLE pay_links ADD COLUMN webhook_url TEXT;")
db.execute("ALTER TABLE pay_links ADD COLUMN success_text TEXT;") await db.execute("ALTER TABLE pay_links ADD COLUMN success_text TEXT;")
db.execute("ALTER TABLE pay_links ADD COLUMN success_url TEXT;") await db.execute("ALTER TABLE pay_links ADD COLUMN success_url TEXT;")
db.execute( await db.execute(
""" """
CREATE TABLE invoices ( CREATE TABLE invoices (
pay_link INTEGER NOT NULL REFERENCES pay_links (id), pay_link INTEGER NOT NULL REFERENCES pay_links (id),
@ -35,14 +35,14 @@ def m002_webhooks_and_success_actions(db):
) )
def m003_min_max_comment_fiat(db): async def m003_min_max_comment_fiat(db):
""" """
Support for min/max amounts, comments and fiat prices that get Support for min/max amounts, comments and fiat prices that get
converted automatically to satoshis based on some API. converted automatically to satoshis based on some API.
""" """
db.execute("ALTER TABLE pay_links ADD COLUMN currency TEXT;") # null = satoshis await db.execute("ALTER TABLE pay_links ADD COLUMN currency TEXT;") # null = satoshis
db.execute("ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;") await db.execute("ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;")
db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;") await db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;")
db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;") await db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;")
db.execute("UPDATE pay_links SET max = min;") await db.execute("UPDATE pay_links SET max = min;")
db.execute("DROP TABLE invoices") await db.execute("DROP TABLE invoices")

View File

@ -3,9 +3,9 @@ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from quart import url_for from quart import url_for
from typing import NamedTuple, Optional, Dict from typing import NamedTuple, Optional, Dict
from sqlite3 import Row from sqlite3 import Row
from lnurl import Lnurl, encode as lnurl_encode from lnurl import Lnurl, encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata from lnurl.types import LnurlPayMetadata # type: ignore
from lnurl.models import LnurlPaySuccessAction, MessageAction, UrlAction from lnurl.models import LnurlPaySuccessAction, MessageAction, UrlAction # type: ignore
class PayLink(NamedTuple): class PayLink(NamedTuple):

View File

@ -1,10 +1,12 @@
import trio # type: ignore import trio # type: ignore
import json
import httpx import httpx
from lnbits.core import db as core_db
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.tasks import run_on_pseudo_request, register_invoice_listener from lnbits.tasks import register_invoice_listener
from .crud import mark_webhook_sent, get_pay_link from .crud import get_pay_link
async def register_listeners(): async def register_listeners():
@ -15,7 +17,7 @@ async def register_listeners():
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan: async for payment in invoice_paid_chan:
await run_on_pseudo_request(on_invoice_paid, payment) await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
@ -27,7 +29,7 @@ async def on_invoice_paid(payment: Payment) -> None:
# this webhook has already been sent # this webhook has already been sent
return return
pay_link = get_pay_link(payment.extra.get("link", -1)) pay_link = await get_pay_link(payment.extra.get("link", -1))
if pay_link and pay_link.webhook_url: if pay_link and pay_link.webhook_url:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
@ -42,6 +44,18 @@ async def on_invoice_paid(payment: Payment) -> None:
}, },
timeout=40, timeout=40,
) )
mark_webhook_sent(payment, r.status_code) await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
mark_webhook_sent(payment, -1) await mark_webhook_sent(payment, -1)
async def mark_webhook_sent(payment: Payment, status: int) -> None:
payment.extra["wh_status"] = status
await core_db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)

View File

@ -17,7 +17,7 @@
<code>[&lt;pay_link_object&gt;, ...]</code> <code>[&lt;pay_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}pay/api/v1/links -H "X-Api-Key: {{ >curl -X GET {{ request.url_root }}lnurlp/api/v1/links -H "X-Api-Key: {{
g.user.wallets[0].inkey }}" g.user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -38,7 +38,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt; -H >curl -X GET {{ request.url_root }}lnurlp/api/v1/links/&lt;pay_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}" "X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@ -63,7 +63,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.url_root }}pay/api/v1/links -d >curl -X POST {{ request.url_root }}lnurlp/api/v1/links -d
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H '{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{ "Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}" g.user.wallets[0].adminkey }}"
@ -93,7 +93,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X PUT {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt; -d >curl -X PUT {{ request.url_root }}lnurlp/api/v1/links/&lt;pay_id&gt; -d
'{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H '{"description": &lt;string&gt;, "amount": &lt;integer&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{ "Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}" g.user.wallets[0].adminkey }}"
@ -120,7 +120,7 @@
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.url_root }}pay/api/v1/links/&lt;pay_id&gt; >curl -X DELETE {{ request.url_root }}lnurlp/api/v1/links/&lt;pay_id&gt;
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>

View File

@ -3,7 +3,7 @@ from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.lnurlp import lnurlp_ext from . import lnurlp_ext
from .crud import get_pay_link from .crud import get_pay_link
@ -16,11 +16,17 @@ async def index():
@lnurlp_ext.route("/<link_id>") @lnurlp_ext.route("/<link_id>")
async def display(link_id): async def display(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") link = await get_pay_link(link_id)
if not link:
abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return await render_template("lnurlp/display.html", link=link) return await render_template("lnurlp/display.html", link=link)
@lnurlp_ext.route("/print/<link_id>") @lnurlp_ext.route("/print/<link_id>")
async def print_qr(link_id): async def print_qr(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") link = await get_pay_link(link_id)
if not link:
abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return await render_template("lnurlp/print_qr.html", link=link) return await render_template("lnurlp/print_qr.html", link=link)

View File

@ -5,7 +5,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.lnurlp import lnurlp_ext # type: ignore from . import lnurlp_ext
from .crud import ( from .crud import (
create_pay_link, create_pay_link,
get_pay_link, get_pay_link,
@ -22,11 +22,11 @@ async def api_links():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
try: try:
return ( return (
jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in get_pay_links(wallet_ids)]), jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in await get_pay_links(wallet_ids)]),
HTTPStatus.OK, HTTPStatus.OK,
) )
except LnurlInvalidUrl: except LnurlInvalidUrl:
@ -39,7 +39,7 @@ async def api_links():
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_link_retrieve(link_id): async def api_link_retrieve(link_id):
link = get_pay_link(link_id) link = await get_pay_link(link_id)
if not link: if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
@ -75,7 +75,7 @@ async def api_link_create_or_update(link_id=None):
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
if link_id: if link_id:
link = get_pay_link(link_id) link = await get_pay_link(link_id)
if not link: if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
@ -83,9 +83,9 @@ async def api_link_create_or_update(link_id=None):
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
link = update_pay_link(link_id, **g.data) link = await update_pay_link(link_id, **g.data)
else: else:
link = create_pay_link(wallet_id=g.wallet.id, **g.data) link = await create_pay_link(wallet_id=g.wallet.id, **g.data)
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED
@ -93,7 +93,7 @@ async def api_link_create_or_update(link_id=None):
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) @lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_link_delete(link_id): async def api_link_delete(link_id):
link = get_pay_link(link_id) link = await get_pay_link(link_id)
if not link: if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
@ -101,7 +101,7 @@ async def api_link_delete(link_id):
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
delete_pay_link(link_id) await delete_pay_link(link_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_paywall")
paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates") paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")

View File

@ -1,45 +1,43 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Paywall from .models import Paywall
def create_paywall( async def create_paywall(
*, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True *, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True
) -> Paywall: ) -> Paywall:
with open_ext_db("paywall") as db: paywall_id = urlsafe_short_hash()
paywall_id = urlsafe_short_hash() await db.execute(
db.execute( """
""" INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers)
INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers) VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?) """,
""", (paywall_id, wallet_id, url, memo, description, amount, int(remembers)),
(paywall_id, wallet_id, url, memo, description, amount, int(remembers)), )
)
return get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
assert paywall, "Newly created paywall couldn't be retrieved"
return paywall
def get_paywall(paywall_id: str) -> Optional[Paywall]: async def get_paywall(paywall_id: str) -> Optional[Paywall]:
with open_ext_db("paywall") as db: row = await db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,))
return Paywall.from_row(row) if row else None return Paywall.from_row(row) if row else None
def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]: async def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("paywall") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,))
return [Paywall.from_row(row) for row in rows] return [Paywall.from_row(row) for row in rows]
def delete_paywall(paywall_id: str) -> None: async def delete_paywall(paywall_id: str) -> None:
with open_ext_db("paywall") as db: await db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))
db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,))

View File

@ -1,11 +1,11 @@
from sqlite3 import OperationalError from sqlalchemy.exc import OperationalError # type: ignore
def m001_initial(db): async def m001_initial(db):
""" """
Initial paywalls table. Initial paywalls table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS paywalls ( CREATE TABLE IF NOT EXISTS paywalls (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -20,16 +20,16 @@ def m001_initial(db):
) )
def m002_redux(db): async def m002_redux(db):
""" """
Creates an improved paywalls table and migrates the existing data. Creates an improved paywalls table and migrates the existing data.
""" """
try: try:
db.execute("SELECT remembers FROM paywalls") await db.execute("SELECT remembers FROM paywalls")
except OperationalError: except OperationalError:
db.execute("ALTER TABLE paywalls RENAME TO paywalls_old") await db.execute("ALTER TABLE paywalls RENAME TO paywalls_old")
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS paywalls ( CREATE TABLE IF NOT EXISTS paywalls (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -44,10 +44,10 @@ def m002_redux(db):
); );
""" """
) )
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)") await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)")
for row in [list(row) for row in db.fetchall("SELECT * FROM paywalls_old")]: for row in [list(row) for row in await db.fetchall("SELECT * FROM paywalls_old")]:
db.execute( await db.execute(
""" """
INSERT INTO paywalls ( INSERT INTO paywalls (
id, id,
@ -62,4 +62,4 @@ def m002_redux(db):
(row[0], row[1], row[3], row[4], row[5], row[6]), (row[0], row[1], row[3], row[4], row[5], row[6]),
) )
db.execute("DROP TABLE paywalls_old") await db.execute("DROP TABLE paywalls_old")

View File

@ -3,7 +3,7 @@ from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.paywall import paywall_ext from . import paywall_ext
from .crud import get_paywall from .crud import get_paywall
@ -16,5 +16,5 @@ async def index():
@paywall_ext.route("/<paywall_id>") @paywall_ext.route("/<paywall_id>")
async def display(paywall_id): async def display(paywall_id):
paywall = get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.") paywall = await get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.")
return await render_template("paywall/display.html", paywall=paywall) return await render_template("paywall/display.html", paywall=paywall)

View File

@ -5,7 +5,7 @@ from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.paywall import paywall_ext from . import paywall_ext
from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
@ -15,9 +15,9 @@ async def api_paywalls():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), HTTPStatus.OK return jsonify([paywall._asdict() for paywall in await get_paywalls(wallet_ids)]), HTTPStatus.OK
@paywall_ext.route("/api/v1/paywalls", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls", methods=["POST"])
@ -32,15 +32,14 @@ async def api_paywalls():
} }
) )
async def api_paywall_create(): async def api_paywall_create():
paywall = create_paywall(wallet_id=g.wallet.id, **g.data) paywall = await create_paywall(wallet_id=g.wallet.id, **g.data)
return jsonify(paywall._asdict()), HTTPStatus.CREATED return jsonify(paywall._asdict()), HTTPStatus.CREATED
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_paywall_delete(paywall_id): async def api_paywall_delete(paywall_id):
paywall = get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if not paywall: if not paywall:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
@ -48,7 +47,7 @@ async def api_paywall_delete(paywall_id):
if paywall.wallet != g.wallet.id: if paywall.wallet != g.wallet.id:
return jsonify({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN
delete_paywall(paywall_id) await delete_paywall(paywall_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -56,14 +55,14 @@ async def api_paywall_delete(paywall_id):
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) @api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
async def api_paywall_create_invoice(paywall_id): async def api_paywall_create_invoice(paywall_id):
paywall = get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if g.data["amount"] < paywall.amount: if g.data["amount"] < paywall.amount:
return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST
try: try:
amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={"tag": "paywall"} wallet_id=paywall.wallet, amount=amount, memo=f"{paywall.memo}", extra={"tag": "paywall"}
) )
except Exception as e: except Exception as e:
@ -75,20 +74,21 @@ async def api_paywall_create_invoice(paywall_id):
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
@api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}})
async def api_paywal_check_invoice(paywall_id): async def api_paywal_check_invoice(paywall_id):
paywall = get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if not paywall: if not paywall:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
try: try:
is_paid = not check_invoice_status(paywall.wallet, g.data["payment_hash"]).pending status = await check_invoice_status(paywall.wallet, g.data["payment_hash"])
is_paid = not status.pending
except Exception: except Exception:
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
if is_paid: if is_paid:
wallet = get_wallet(paywall.wallet) wallet = await get_wallet(paywall.wallet)
payment = wallet.get_payment(g.data["payment_hash"]) payment = await wallet.get_payment(g.data["payment_hash"])
payment.set_pending(False) await payment.set_pending(False)
return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_tpos")
tpos_ext: Blueprint = Blueprint("tpos", __name__, static_folder="static", template_folder="templates") tpos_ext: Blueprint = Blueprint("tpos", __name__, static_folder="static", template_folder="templates")

View File

@ -1,43 +1,40 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import TPoS from .models import TPoS
def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS: async def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS:
with open_ext_db("tpos") as db: tpos_id = urlsafe_short_hash()
tpos_id = urlsafe_short_hash() await db.execute(
db.execute( """
""" INSERT INTO tposs (id, wallet, name, currency)
INSERT INTO tposs (id, wallet, name, currency) VALUES (?, ?, ?, ?)
VALUES (?, ?, ?, ?) """,
""", (tpos_id, wallet_id, name, currency),
(tpos_id, wallet_id, name, currency), )
)
return get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
assert tpos, "Newly created tpos couldn't be retrieved"
return tpos
def get_tpos(tpos_id: str) -> Optional[TPoS]: async def get_tpos(tpos_id: str) -> Optional[TPoS]:
with open_ext_db("tpos") as db: row = await db.fetchone("SELECT * FROM tposs WHERE id = ?", (tpos_id,))
row = db.fetchone("SELECT * FROM tposs WHERE id = ?", (tpos_id,))
return TPoS.from_row(row) if row else None return TPoS.from_row(row) if row else None
def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("tpos") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM tposs WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM tposs WHERE wallet IN ({q})", (*wallet_ids,))
return [TPoS.from_row(row) for row in rows] return [TPoS.from_row(row) for row in rows]
def delete_tpos(tpos_id: str) -> None: async def delete_tpos(tpos_id: str) -> None:
with open_ext_db("tpos") as db: await db.execute("DELETE FROM tposs WHERE id = ?", (tpos_id,))
db.execute("DELETE FROM tposs WHERE id = ?", (tpos_id,))

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Initial tposs table. Initial tposs table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS tposs ( CREATE TABLE IF NOT EXISTS tposs (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,

View File

@ -3,7 +3,7 @@ from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.tpos import tpos_ext from . import tpos_ext
from .crud import get_tpos from .crud import get_tpos
@ -16,6 +16,8 @@ async def index():
@tpos_ext.route("/<tpos_id>") @tpos_ext.route("/<tpos_id>")
async def tpos(tpos_id): async def tpos(tpos_id):
tpos = get_tpos(tpos_id) or abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.") tpos = await get_tpos(tpos_id)
if not tpos:
abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
return await render_template("tpos/tpos.html", tpos=tpos) return await render_template("tpos/tpos.html", tpos=tpos)

View File

@ -5,7 +5,7 @@ from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.tpos import tpos_ext from . import tpos_ext
from .crud import create_tpos, get_tpos, get_tposs, delete_tpos from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
@ -15,9 +15,9 @@ async def api_tposs():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = await get_user(g.wallet.user).wallet_ids
return jsonify([tpos._asdict() for tpos in get_tposs(wallet_ids)]), HTTPStatus.OK return jsonify([tpos._asdict() for tpos in await get_tposs(wallet_ids)]), HTTPStatus.OK
@tpos_ext.route("/api/v1/tposs", methods=["POST"]) @tpos_ext.route("/api/v1/tposs", methods=["POST"])
@ -29,15 +29,14 @@ async def api_tposs():
} }
) )
async def api_tpos_create(): async def api_tpos_create():
tpos = create_tpos(wallet_id=g.wallet.id, **g.data) tpos = await create_tpos(wallet_id=g.wallet.id, **g.data)
return jsonify(tpos._asdict()), HTTPStatus.CREATED return jsonify(tpos._asdict()), HTTPStatus.CREATED
@tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
async def api_tpos_delete(tpos_id): async def api_tpos_delete(tpos_id):
tpos = get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
if not tpos: if not tpos:
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
@ -45,7 +44,7 @@ async def api_tpos_delete(tpos_id):
if tpos.wallet != g.wallet.id: if tpos.wallet != g.wallet.id:
return jsonify({"message": "Not your TPoS."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your TPoS."}), HTTPStatus.FORBIDDEN
delete_tpos(tpos_id) await delete_tpos(tpos_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -53,13 +52,13 @@ async def api_tpos_delete(tpos_id):
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) @api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
async def api_tpos_create_invoice(tpos_id): async def api_tpos_create_invoice(tpos_id):
tpos = get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
if not tpos: if not tpos:
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try: try:
payment_hash, payment_request = create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"{tpos.name}", extra={"tag": "tpos"} wallet_id=tpos.wallet, amount=g.data["amount"], memo=f"{tpos.name}", extra={"tag": "tpos"}
) )
except Exception as e: except Exception as e:
@ -70,21 +69,22 @@ async def api_tpos_create_invoice(tpos_id):
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<payment_hash>", methods=["GET"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<payment_hash>", methods=["GET"])
async def api_tpos_check_invoice(tpos_id, payment_hash): async def api_tpos_check_invoice(tpos_id, payment_hash):
tpos = get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
if not tpos: if not tpos:
return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "TPoS does not exist."}), HTTPStatus.NOT_FOUND
try: try:
is_paid = not check_invoice_status(tpos.wallet, payment_hash).pending status = await check_invoice_status(tpos.wallet, payment_hash)
is_paid = not status.pending
except Exception as exc: except Exception as exc:
print(exc) print(exc)
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
if is_paid: if is_paid:
wallet = get_wallet(tpos.wallet) wallet = await get_wallet(tpos.wallet)
payment = wallet.get_payment(payment_hash) payment = await wallet.get_payment(payment_hash)
payment.set_pending(False) await payment.set_pending(False)
return jsonify({"paid": True}), HTTPStatus.OK return jsonify({"paid": True}), HTTPStatus.OK

View File

@ -1,5 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_usermanager")
usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates") usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates")

View File

@ -1,8 +1,7 @@
from lnbits.db import open_ext_db from typing import Optional, List
from .models import Users, Wallets
from typing import Optional
from ...core.crud import ( from lnbits.core.models import Payment
from lnbits.core.crud import (
create_account, create_account,
get_user, get_user,
get_wallet_payments, get_wallet_payments,
@ -10,106 +9,91 @@ from ...core.crud import (
delete_wallet, delete_wallet,
) )
from . import db
###Users from .models import Users, Wallets
def create_usermanager_user(user_name: str, wallet_name: str, admin_id: str) -> Users: ### Users
user = get_user(create_account().id)
wallet = create_wallet(user_id=user.id, wallet_name=wallet_name)
with open_ext_db("usermanager") as db:
db.execute(
"""
INSERT INTO users (id, name, admin)
VALUES (?, ?, ?)
""",
(user.id, user_name, admin_id),
)
db.execute(
"""
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey),
)
return get_usermanager_user(user.id)
def get_usermanager_user(user_id: str) -> Users: async def create_usermanager_user(user_name: str, wallet_name: str, admin_id: str) -> Users:
with open_ext_db("usermanager") as db: account = await create_account()
user = await get_user(account.id)
assert user, "Newly created user couldn't be retrieved"
row = db.fetchone("SELECT * FROM users WHERE id = ?", (user_id,)) wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
await db.execute(
"""
INSERT INTO users (id, name, admin)
VALUES (?, ?, ?)
""",
(user.id, user_name, admin_id),
)
await db.execute(
"""
INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey),
)
user_created = await get_usermanager_user(user.id)
assert user_created, "Newly created user couldn't be retrieved"
return user_created
async def get_usermanager_user(user_id: str) -> Optional[Users]:
row = await db.fetchone("SELECT * FROM users WHERE id = ?", (user_id,))
return Users(**row) if row else None return Users(**row) if row else None
def get_usermanager_users(user_id: str) -> Users: async def get_usermanager_users(user_id: str) -> List[Users]:
rows = await db.fetchall("SELECT * FROM users WHERE admin = ?", (user_id,))
with open_ext_db("usermanager") as db:
rows = db.fetchall("SELECT * FROM users WHERE admin = ?", (user_id,))
return [Users(**row) for row in rows] return [Users(**row) for row in rows]
def delete_usermanager_user(user_id: str) -> None: async def delete_usermanager_user(user_id: str) -> None:
row = get_usermanager_wallets(user_id) wallets = await get_usermanager_wallets(user_id)
print("test") for wallet in wallets:
with open_ext_db("usermanager") as db: await delete_wallet(user_id=user_id, wallet_id=wallet.id)
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
row await db.execute("DELETE FROM users WHERE id = ?", (user_id,))
for r in row: await db.execute("DELETE FROM wallets WHERE user = ?", (user_id,))
delete_wallet(user_id=user_id, wallet_id=r.id)
with open_ext_db("usermanager") as dbb:
dbb.execute("DELETE FROM wallets WHERE user = ?", (user_id,))
###Wallets ### Wallets
def create_usermanager_wallet(user_id: str, wallet_name: str, admin_id: str) -> Wallets: async def create_usermanager_wallet(user_id: str, wallet_name: str, admin_id: str) -> Wallets:
wallet = create_wallet(user_id=user_id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
with open_ext_db("usermanager") as db: await db.execute(
"""
db.execute( INSERT INTO wallets (id, admin, name, user, adminkey, inkey)
""" VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO wallets (id, admin, name, user, adminkey, inkey) """,
VALUES (?, ?, ?, ?, ?, ?) (wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey),
""", )
(wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey), wallet_created = await get_usermanager_wallet(wallet.id)
) assert wallet_created, "Newly created wallet couldn't be retrieved"
return wallet_created
return get_usermanager_wallet(wallet.id)
def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]: async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]:
with open_ext_db("usermanager") as db: row = await db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
row = db.fetchone("SELECT * FROM wallets WHERE id = ?", (wallet_id,))
return Wallets(**row) if row else None return Wallets(**row) if row else None
def get_usermanager_wallets(user_id: str) -> Wallets: async def get_usermanager_wallets(user_id: str) -> List[Wallets]:
rows = await db.fetchall("SELECT * FROM wallets WHERE admin = ?", (user_id,))
with open_ext_db("usermanager") as db:
rows = db.fetchall("SELECT * FROM wallets WHERE admin = ?", (user_id,))
return [Wallets(**row) for row in rows] return [Wallets(**row) for row in rows]
def get_usermanager_wallet_transactions(wallet_id: str) -> Users: async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]:
return get_wallet_payments(wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True) return await get_wallet_payments(wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True)
def get_usermanager_wallet_balances(user_id: str) -> Users: async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:
user = get_user(user_id) await delete_wallet(user_id=user_id, wallet_id=wallet_id)
return user.wallets await db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,))
def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:
delete_wallet(user_id=user_id, wallet_id=wallet_id)
with open_ext_db("usermanager") as db:
db.execute("DELETE FROM wallets WHERE id = ?", (wallet_id,))

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Initial users table. Initial users table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -17,7 +17,7 @@ def m001_initial(db):
""" """
Initial wallets table. Initial wallets table.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS wallets ( CREATE TABLE IF NOT EXISTS wallets (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,

View File

@ -1,6 +1,8 @@
from quart import g, render_template from quart import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.usermanager import usermanager_ext
from . import usermanager_ext
@usermanager_ext.route("/") @usermanager_ext.route("/")

View File

@ -4,13 +4,12 @@ from http import HTTPStatus
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.usermanager import usermanager_ext from . import usermanager_ext
from .crud import ( from .crud import (
create_usermanager_user, create_usermanager_user,
get_usermanager_user, get_usermanager_user,
get_usermanager_users, get_usermanager_users,
get_usermanager_wallet_transactions, get_usermanager_wallet_transactions,
get_usermanager_wallet_balances,
delete_usermanager_user, delete_usermanager_user,
create_usermanager_wallet, create_usermanager_wallet,
get_usermanager_wallet, get_usermanager_wallet,
@ -27,7 +26,7 @@ from lnbits.core import update_user_extension
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_users(): async def api_usermanager_users():
user_id = g.wallet.user user_id = g.wallet.user
return jsonify([user._asdict() for user in get_usermanager_users(user_id)]), HTTPStatus.OK return jsonify([user._asdict() for user in await get_usermanager_users(user_id)]), HTTPStatus.OK
@usermanager_ext.route("/api/v1/users", methods=["POST"]) @usermanager_ext.route("/api/v1/users", methods=["POST"])
@ -40,17 +39,17 @@ async def api_usermanager_users():
} }
) )
async def api_usermanager_users_create(): async def api_usermanager_users_create():
user = create_usermanager_user(g.data["user_name"], g.data["wallet_name"], g.data["admin_id"]) user = await create_usermanager_user(g.data["user_name"], g.data["wallet_name"], g.data["admin_id"])
return jsonify(user._asdict()), HTTPStatus.CREATED return jsonify(user._asdict()), HTTPStatus.CREATED
@usermanager_ext.route("/api/v1/users/<user_id>", methods=["DELETE"]) @usermanager_ext.route("/api/v1/users/<user_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_users_delete(user_id): async def api_usermanager_users_delete(user_id):
user = get_usermanager_user(user_id) user = await get_usermanager_user(user_id)
if not user: if not user:
return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND
delete_usermanager_user(user_id) await delete_usermanager_user(user_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -67,7 +66,7 @@ async def api_usermanager_users_delete(user_id):
} }
) )
async def api_usermanager_activate_extension(): async def api_usermanager_activate_extension():
user = get_user(g.data["userid"]) user = await get_user(g.data["userid"])
if not user: if not user:
return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND return jsonify({"message": "no such user"}), HTTPStatus.NOT_FOUND
update_user_extension(user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"]) update_user_extension(user_id=g.data["userid"], extension=g.data["extension"], active=g.data["active"])
@ -81,7 +80,7 @@ async def api_usermanager_activate_extension():
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_wallets(): async def api_usermanager_wallets():
user_id = g.wallet.user user_id = g.wallet.user
return jsonify([wallet._asdict() for wallet in get_usermanager_wallets(user_id)]), HTTPStatus.OK return jsonify([wallet._asdict() for wallet in await get_usermanager_wallets(user_id)]), HTTPStatus.OK
@usermanager_ext.route("/api/v1/wallets", methods=["POST"]) @usermanager_ext.route("/api/v1/wallets", methods=["POST"])
@ -94,31 +93,28 @@ async def api_usermanager_wallets():
} }
) )
async def api_usermanager_wallets_create(): async def api_usermanager_wallets_create():
user = create_usermanager_wallet(g.data["user_id"], g.data["wallet_name"], g.data["admin_id"]) user = await create_usermanager_wallet(g.data["user_id"], g.data["wallet_name"], g.data["admin_id"])
return jsonify(user._asdict()), HTTPStatus.CREATED return jsonify(user._asdict()), HTTPStatus.CREATED
@usermanager_ext.route("/api/v1/wallets<wallet_id>", methods=["GET"]) @usermanager_ext.route("/api/v1/wallets<wallet_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_wallet_transactions(wallet_id): async def api_usermanager_wallet_transactions(wallet_id):
return jsonify(await get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK
return jsonify(get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK
@usermanager_ext.route("/api/v1/wallets/<user_id>", methods=["GET"]) @usermanager_ext.route("/api/v1/wallets/<user_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_wallet_balances(user_id): async def api_usermanager_wallet(user_id):
return jsonify(get_usermanager_wallet_balances(user_id)), HTTPStatus.OK return jsonify(await get_usermanager_wallets(user_id)), HTTPStatus.OK
@usermanager_ext.route("/api/v1/wallets/<wallet_id>", methods=["DELETE"]) @usermanager_ext.route("/api/v1/wallets/<wallet_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
async def api_usermanager_wallets_delete(wallet_id): async def api_usermanager_wallets_delete(wallet_id):
wallet = get_usermanager_wallet(wallet_id) wallet = await get_usermanager_wallet(wallet_id)
print(wallet.id)
if not wallet: if not wallet:
return jsonify({"message": "Wallet does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Wallet does not exist."}), HTTPStatus.NOT_FOUND
delete_usermanager_wallet(wallet_id, wallet.user) await delete_usermanager_wallet(wallet_id, wallet.user)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT

View File

@ -1,4 +1,7 @@
from quart import Blueprint from quart import Blueprint
from lnbits.db import Database
db = Database("ext_withdraw")
withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates") withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates")

View File

@ -1,12 +1,12 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import WithdrawLink from .models import WithdrawLink
def create_withdraw_link( async def create_withdraw_link(
*, *,
wallet_id: str, wallet_id: str,
title: str, title: str,
@ -17,49 +17,47 @@ def create_withdraw_link(
is_unique: bool, is_unique: bool,
usescsv: str, usescsv: str,
) -> WithdrawLink: ) -> WithdrawLink:
link_id = urlsafe_short_hash()
with open_ext_db("withdraw") as db: await db.execute(
"""
link_id = urlsafe_short_hash() INSERT INTO withdraw_link (
db.execute( id,
""" wallet,
INSERT INTO withdraw_link ( title,
id, min_withdrawable,
wallet, max_withdrawable,
title, uses,
min_withdrawable, wait_time,
max_withdrawable, is_unique,
uses, unique_hash,
wait_time, k1,
is_unique, open_time,
unique_hash, usescsv
k1,
open_time,
usescsv
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
link_id,
wallet_id,
title,
min_withdrawable,
max_withdrawable,
uses,
wait_time,
int(is_unique),
urlsafe_short_hash(),
urlsafe_short_hash(),
int(datetime.now().timestamp()) + wait_time,
usescsv,
),
) )
return get_withdraw_link(link_id, 0) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
link_id,
wallet_id,
title,
min_withdrawable,
max_withdrawable,
uses,
wait_time,
int(is_unique),
urlsafe_short_hash(),
urlsafe_short_hash(),
int(datetime.now().timestamp()) + wait_time,
usescsv,
),
)
link = await get_withdraw_link(link_id, 0)
assert link, "Newly created link couldn't be retrieved"
return link
def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]: async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]:
with open_ext_db("withdraw") as db: row = await db.fetchone("SELECT * FROM withdraw_link WHERE id = ?", (link_id,))
row = db.fetchone("SELECT * FROM withdraw_link WHERE id = ?", (link_id,))
link = [] link = []
for item in row: for item in row:
link.append(item) link.append(item)
@ -67,39 +65,34 @@ def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]:
return WithdrawLink._make(link) return WithdrawLink._make(link)
def get_withdraw_link_by_hash(unique_hash: str, num=0) -> Optional[WithdrawLink]: async def get_withdraw_link_by_hash(unique_hash: str, num=0) -> Optional[WithdrawLink]:
with open_ext_db("withdraw") as db: row = await db.fetchone("SELECT * FROM withdraw_link WHERE unique_hash = ?", (unique_hash,))
row = db.fetchone("SELECT * FROM withdraw_link WHERE unique_hash = ?", (unique_hash,)) link = []
link = [] for item in row:
for item in row: link.append(item)
link.append(item)
link.append(num) link.append(num)
return WithdrawLink._make(link) return WithdrawLink._make(link)
def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[WithdrawLink]: async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[WithdrawLink]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
with open_ext_db("withdraw") as db: q = ",".join(["?"] * len(wallet_ids))
q = ",".join(["?"] * len(wallet_ids)) rows = await db.fetchall(f"SELECT * FROM withdraw_link WHERE wallet IN ({q})", (*wallet_ids,))
rows = db.fetchall(f"SELECT * FROM withdraw_link WHERE wallet IN ({q})", (*wallet_ids,))
return [WithdrawLink.from_row(row) for row in rows] return [WithdrawLink.from_row(row) for row in rows]
def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]: async def update_withdraw_link(link_id: str, **kwargs) -> Optional[WithdrawLink]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("withdraw") as db: await db.execute(f"UPDATE withdraw_link SET {q} WHERE id = ?", (*kwargs.values(), link_id))
db.execute(f"UPDATE withdraw_link SET {q} WHERE id = ?", (*kwargs.values(), link_id)) row = await db.fetchone("SELECT * FROM withdraw_link WHERE id = ?", (link_id,))
row = db.fetchone("SELECT * FROM withdraw_link WHERE id = ?", (link_id,))
return WithdrawLink.from_row(row) if row else None return WithdrawLink.from_row(row) if row else None
def delete_withdraw_link(link_id: str) -> None: async def delete_withdraw_link(link_id: str) -> None:
with open_ext_db("withdraw") as db: await db.execute("DELETE FROM withdraw_link WHERE id = ?", (link_id,))
db.execute("DELETE FROM withdraw_link WHERE id = ?", (link_id,))
def chunks(lst, n): def chunks(lst, n):

View File

@ -1,8 +1,8 @@
def m001_initial(db): async def m001_initial(db):
""" """
Creates an improved withdraw table and migrates the existing data. Creates an improved withdraw table and migrates the existing data.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS withdraw_links ( CREATE TABLE IF NOT EXISTS withdraw_links (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -23,11 +23,11 @@ def m001_initial(db):
) )
def m002_change_withdraw_table(db): async def m002_change_withdraw_table(db):
""" """
Creates an improved withdraw table and migrates the existing data. Creates an improved withdraw table and migrates the existing data.
""" """
db.execute( await db.execute(
""" """
CREATE TABLE IF NOT EXISTS withdraw_link ( CREATE TABLE IF NOT EXISTS withdraw_link (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -46,10 +46,10 @@ def m002_change_withdraw_table(db):
); );
""" """
) )
db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON withdraw_link (wallet)") await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON withdraw_link (wallet)")
db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_hash_idx ON withdraw_link (unique_hash)") await db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_hash_idx ON withdraw_link (unique_hash)")
for row in [list(row) for row in db.fetchall("SELECT * FROM withdraw_links")]: for row in [list(row) for row in await db.fetchall("SELECT * FROM withdraw_links")]:
usescsv = "" usescsv = ""
for i in range(row[5]): for i in range(row[5]):
@ -58,7 +58,7 @@ def m002_change_withdraw_table(db):
else: else:
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
db.execute( await db.execute(
""" """
INSERT INTO withdraw_link ( INSERT INTO withdraw_link (
id, id,
@ -93,4 +93,4 @@ def m002_change_withdraw_table(db):
usescsv, usescsv,
), ),
) )
db.execute("DROP TABLE withdraw_links") await db.execute("DROP TABLE withdraw_links")

View File

@ -1,5 +1,5 @@
from quart import url_for from quart import url_for
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple from typing import NamedTuple
import shortuuid # type: ignore import shortuuid # type: ignore

View File

@ -3,7 +3,7 @@ from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.withdraw import withdraw_ext from . import withdraw_ext
from .crud import get_withdraw_link, chunks from .crud import get_withdraw_link, chunks
@ -16,19 +16,19 @@ async def index():
@withdraw_ext.route("/<link_id>") @withdraw_ext.route("/<link_id>")
async def display(link_id): async def display(link_id):
link = get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") link = await get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.")
return await render_template("withdraw/display.html", link=link, unique=True) return await render_template("withdraw/display.html", link=link, unique=True)
@withdraw_ext.route("/print/<link_id>") @withdraw_ext.route("/print/<link_id>")
async def print_qr(link_id): async def print_qr(link_id):
link = get_withdraw_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") link = await get_withdraw_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.")
if link.uses == 0: if link.uses == 0:
return await render_template("withdraw/print_qr.html", link=link, unique=False) return await render_template("withdraw/print_qr.html", link=link, unique=False)
links = [] links = []
count = 0 count = 0
for x in link.usescsv.split(","): for x in link.usescsv.split(","):
linkk = get_withdraw_link(link_id, count) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") linkk = await get_withdraw_link(link_id, count) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.")
links.append(str(linkk.lnurl)) links.append(str(linkk.lnurl))
count = count + 1 count = count + 1
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))

View File

@ -1,14 +1,14 @@
from datetime import datetime from datetime import datetime
from quart import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
import shortuuid # type: ignore import shortuuid # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.withdraw import withdraw_ext from . import withdraw_ext
from .crud import ( from .crud import (
create_withdraw_link, create_withdraw_link,
get_withdraw_link, get_withdraw_link,
@ -25,10 +25,18 @@ async def api_links():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
try: try:
return ( return (
jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in get_withdraw_links(wallet_ids)]), jsonify(
[
{
**link._asdict(),
**{"lnurl": link.lnurl},
}
for link in await get_withdraw_links(wallet_ids)
]
),
HTTPStatus.OK, HTTPStatus.OK,
) )
except LnurlInvalidUrl: except LnurlInvalidUrl:
@ -41,7 +49,7 @@ async def api_links():
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"]) @withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
async def api_link_retrieve(link_id): async def api_link_retrieve(link_id):
link = get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND
@ -82,21 +90,22 @@ async def api_link_create_or_update(link_id=None):
usescsv = usescsv[1:] usescsv = usescsv[1:]
if link_id: if link_id:
link = get_withdraw_link(link_id, 0) link = await get_withdraw_link(link_id, 0)
if not link: if not link:
return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN
link = update_withdraw_link(link_id, **g.data, usescsv=usescsv, used=0) link = await update_withdraw_link(link_id, **g.data, usescsv=usescsv, used=0)
else: else:
link = create_withdraw_link(wallet_id=g.wallet.id, **g.data, usescsv=usescsv) link = await create_withdraw_link(wallet_id=g.wallet.id, **g.data, usescsv=usescsv)
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK if link_id else HTTPStatus.CREATED
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) @withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
async def api_link_delete(link_id): async def api_link_delete(link_id):
link = get_withdraw_link(link_id) link = await get_withdraw_link(link_id)
if not link: if not link:
return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Withdraw link does not exist."}), HTTPStatus.NOT_FOUND
@ -104,7 +113,7 @@ async def api_link_delete(link_id):
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN
delete_withdraw_link(link_id) await delete_withdraw_link(link_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -114,7 +123,7 @@ async def api_link_delete(link_id):
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"])
async def api_lnurl_response(unique_hash): async def api_lnurl_response(unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = await get_withdraw_link_by_hash(unique_hash)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK
@ -125,7 +134,7 @@ async def api_lnurl_response(unique_hash):
for x in range(1, link.uses - link.used): for x in range(1, link.uses - link.used):
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
link = update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv) link = await update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@ -135,7 +144,7 @@ async def api_lnurl_response(unique_hash):
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"])
async def api_lnurl_multi_response(unique_hash, id_unique_hash): async def api_lnurl_multi_response(unique_hash, id_unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = await get_withdraw_link_by_hash(unique_hash)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK
@ -156,13 +165,13 @@ async def api_lnurl_multi_response(unique_hash, id_unique_hash):
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK
usescsv = usescsv[1:] usescsv = usescsv[1:]
link = update_withdraw_link(link.id, usescsv=usescsv) link = await update_withdraw_link(link.id, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"])
async def api_lnurl_callback(unique_hash): async def api_lnurl_callback(unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = await get_withdraw_link_by_hash(unique_hash)
k1 = request.args.get("k1", type=str) k1 = request.args.get("k1", type=str)
payment_request = request.args.get("pr", type=str) payment_request = request.args.get("pr", type=str)
now = int(datetime.now().timestamp()) now = int(datetime.now().timestamp())
@ -180,7 +189,7 @@ async def api_lnurl_callback(unique_hash):
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK
try: try:
pay_invoice( await pay_invoice(
wallet_id=link.wallet, wallet_id=link.wallet,
payment_request=payment_request, payment_request=payment_request,
max_sat=link.max_withdrawable, max_sat=link.max_withdrawable,
@ -189,12 +198,10 @@ async def api_lnurl_callback(unique_hash):
changes = {"open_time": link.wait_time + now, "used": link.used + 1} changes = {"open_time": link.wait_time + now, "used": link.used + 1}
update_withdraw_link(link.id, **changes) await update_withdraw_link(link.id, **changes)
except ValueError as e: except ValueError as e:
return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK
except PermissionError: except PermissionError:
return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "Withdraw link is empty."}), HTTPStatus.OK
except Exception as e:
return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK
return jsonify({"status": "OK"}), HTTPStatus.OK return jsonify({"status": "OK"}), HTTPStatus.OK

View File

@ -1,13 +1,9 @@
import trio # type: ignore import trio # type: ignore
from http import HTTPStatus from http import HTTPStatus
from typing import Optional, List, Callable from typing import Optional, List, Callable
from quart import Request, g
from quart_trio import QuartTrio from quart_trio import QuartTrio
from werkzeug.datastructures import Headers
from lnbits.db import open_db
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.core.crud import get_standalone_payment from lnbits.core.crud import get_standalone_payment
main_app: Optional[QuartTrio] = None main_app: Optional[QuartTrio] = None
@ -37,28 +33,6 @@ async def send_push_promise(a, b) -> None:
pass pass
async def run_on_pseudo_request(func: Callable, *args):
fk = Request(
"GET",
"http",
"/background/pseudo",
b"",
Headers([("host", "lnbits.background")]),
"",
"1.1",
send_push_promise=send_push_promise,
)
assert main_app
async def run():
async with main_app.request_context(fk):
with open_db() as g.db: # type: ignore
await func(*args)
async with trio.open_nursery() as nursery:
nursery.start_soon(run)
invoice_listeners: List[trio.MemorySendChannel] = [] invoice_listeners: List[trio.MemorySendChannel] = []
@ -81,18 +55,20 @@ internal_invoice_paid, internal_invoice_received = trio.open_memory_channel(0)
async def internal_invoice_listener(): async def internal_invoice_listener():
async for checking_id in internal_invoice_received: async with trio.open_nursery() as nursery:
await run_on_pseudo_request(invoice_callback_dispatcher, checking_id) async for checking_id in internal_invoice_received:
nursery.start_soon(invoice_callback_dispatcher, checking_id)
async def invoice_listener(): async def invoice_listener():
async for checking_id in WALLET.paid_invoices_stream(): async with trio.open_nursery() as nursery:
await run_on_pseudo_request(invoice_callback_dispatcher, checking_id) async for checking_id in WALLET.paid_invoices_stream():
nursery.start_soon(invoice_callback_dispatcher, checking_id)
async def invoice_callback_dispatcher(checking_id: str): async def invoice_callback_dispatcher(checking_id: str):
payment = get_standalone_payment(checking_id) payment = await get_standalone_payment(checking_id)
if payment and payment.is_in: if payment and payment.is_in:
payment.set_pending(False) await payment.set_pending(False)
for send_chan in invoice_listeners: for send_chan in invoice_listeners:
await send_chan.send(payment) await send_chan.send(payment)