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:
parent
f877dde2b0
commit
d3fc52cd49
2
Makefile
2
Makefile
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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.")
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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))
|
||||||
|
|
111
lnbits/db.py
111
lnbits/db.py
|
@ -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")
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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("/")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
def migrate():
|
async def migrate():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -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("/")
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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),
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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),
|
||||||
|
)
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<code>[<pay_link_object>, ...]</code>
|
<code>[<pay_link_object>, ...]</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": <string>}</code>
|
<code>{"lnurl": <string>}</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/<pay_id> -H
|
>curl -X GET {{ request.url_root }}lnurlp/api/v1/links/<pay_id> -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": <string>}</code>
|
<code>{"lnurl": <string>}</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": <string>, "amount": <integer>}' -H
|
'{"description": <string>, "amount": <integer>}' -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": <string>}</code>
|
<code>{"lnurl": <string>}</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/<pay_id> -d
|
>curl -X PUT {{ request.url_root }}lnurlp/api/v1/links/<pay_id> -d
|
||||||
'{"description": <string>, "amount": <integer>}' -H
|
'{"description": <string>, "amount": <integer>}' -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/<pay_id>
|
>curl -X DELETE {{ request.url_root }}lnurlp/api/v1/links/<pay_id>
|
||||||
-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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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("/")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user