remove exception to black line-length and reformat.

This commit is contained in:
fiatjaf 2021-03-24 00:40:32 -03:00
parent 3333f1f3f3
commit 42bd5ea989
92 changed files with 1341 additions and 330 deletions

View File

@ -10,7 +10,14 @@ 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 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

View File

@ -9,7 +9,12 @@ 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 .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 ( from .tasks import (
run_deferred_async, run_deferred_async,
@ -57,7 +62,9 @@ def check_funding_source(app: QuartTrio) -> None:
RuntimeWarning, RuntimeWarning,
) )
else: else:
print(f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat.") print(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
)
def register_blueprints(app: QuartTrio) -> None: def register_blueprints(app: QuartTrio) -> None:
@ -75,7 +82,9 @@ def register_blueprints(app: QuartTrio) -> None:
app.register_blueprint(bp, url_prefix=f"/{ext.code}") app.register_blueprint(bp, url_prefix=f"/{ext.code}")
except Exception: except Exception:
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.") raise ImportError(
f"Please make sure that the extension `{ext.code}` follows conventions."
)
def register_commands(app: QuartTrio): def register_commands(app: QuartTrio):

View File

@ -106,7 +106,9 @@ def decode(pr: str) -> Invoice:
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1) key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string) key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
else: else:
keys = VerifyingKey.from_public_key_recovery(sig, message, SECP256k1, hashlib.sha256) keys = VerifyingKey.from_public_key_recovery(
sig, message, SECP256k1, hashlib.sha256
)
signaling_byte = signature[64] signaling_byte = signature[64]
key = keys[int(signaling_byte)] key = keys[int(signaling_byte)]
invoice.payee = key.to_string("compressed").hex() invoice.payee = key.to_string("compressed").hex()

View File

@ -7,7 +7,12 @@ import os
from sqlalchemy.exc import OperationalError # type: ignore from sqlalchemy.exc import OperationalError # type: ignore
from .core import db as core_db, migrations as core_migrations from .core import db as core_db, migrations as core_migrations
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
@ -71,18 +76,23 @@ async def migrate_databases():
print(f"running migration {db_name}.{version}") print(f"running migration {db_name}.{version}")
await migrate(db) await migrate(db)
await core_conn.execute( await core_conn.execute(
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)", (db_name, version) "INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)",
(db_name, version),
) )
await run_migration(core_conn, core_migrations) await run_migration(core_conn, core_migrations)
for ext in get_valid_extensions(): for ext in get_valid_extensions():
try: try:
ext_migrations = importlib.import_module(f"lnbits.extensions.{ext.code}.migrations") ext_migrations = importlib.import_module(
f"lnbits.extensions.{ext.code}.migrations"
)
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
await run_migration(ext_db, ext_migrations) await run_migration(ext_db, ext_migrations)
except ImportError: except ImportError:
raise ImportError(f"Please make sure that the extension `{ext.code}` has a migrations file.") raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
await core_txn.commit() await core_txn.commit()
await core_conn.close() await core_conn.close()

View File

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

View File

@ -25,7 +25,9 @@ async def create_account() -> User:
async def get_account(user_id: str) -> Optional[User]: async def get_account(user_id: str) -> Optional[User]:
row = await 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
@ -34,7 +36,9 @@ async def get_user(user_id: str) -> Optional[User]:
user = await 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 = await 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 = await 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
@ -45,7 +49,15 @@ async def get_user(user_id: str) -> Optional[User]:
) )
return ( return (
User(**{**user, **{"extensions": [e[0] for e in extensions], "wallets": [Wallet(**w) for w in wallets]}}) User(
**{
**user,
**{
"extensions": [e[0] for e in extensions],
"wallets": [Wallet(**w) for w in wallets],
},
}
)
if user if user
else None else None
) )
@ -72,7 +84,13 @@ async def create_wallet(*, user_id: str, wallet_name: Optional[str] = None) -> W
INSERT INTO wallets (id, name, user, adminkey, inkey) INSERT INTO wallets (id, name, user, adminkey, inkey)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
(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 = await get_wallet(wallet_id=wallet_id) new_wallet = await get_wallet(wallet_id=wallet_id)
@ -280,7 +298,9 @@ async def create_payment(
int(pending), int(pending),
memo, memo,
fee, fee,
json.dumps(extra) if extra and extra != {} and type(extra) is dict else None, json.dumps(extra)
if extra and extra != {} and type(extra) is dict
else None,
webhook, webhook,
), ),
) )

View File

@ -111,7 +111,12 @@ async def m002_add_fields_to_apipayments(db):
UPDATE apipayments SET extra = ?, memo = ? UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ? WHERE checking_id = ? AND memo = ?
""", """,
(json.dumps({"tag": ext}), new, row["checking_id"], row["memo"]), (
json.dumps({"tag": ext}),
new,
row["checking_id"],
row["memo"],
),
) )
break break
except OperationalError: except OperationalError:

View File

@ -127,7 +127,9 @@ class Payment(NamedTuple):
@property @property
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_"
)
async 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

View File

@ -18,7 +18,14 @@ from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus, PaymentResponse from lnbits.wallets.base import PaymentStatus, PaymentResponse
from . import db 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,
)
async def create_invoice( async def create_invoice(
@ -101,7 +108,9 @@ async def pay_invoice(
internal_checking_id = await 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
await 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
@ -143,12 +152,16 @@ async def pay_invoice(
else: else:
await delete_payment(temp_id) await delete_payment(temp_id)
await db.commit() await db.commit()
raise Exception(payment.error_message or "Failed to pay_invoice on backend.") raise Exception(
payment.error_message or "Failed to pay_invoice on backend."
)
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 = await create_invoice( _, payment_request = await create_invoice(
wallet_id=wallet_id, wallet_id=wallet_id,
amount=res.max_sats, amount=res.max_sats,
@ -159,7 +172,10 @@ 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},
},
) )

View File

@ -41,8 +41,18 @@ async def api_payments():
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"amount": {"type": "integer", "min": 1, "required": True}, "amount": {"type": "integer", "min": 1, "required": True},
"memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"}, "memo": {
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"}, "type": "string",
"empty": False,
"required": True,
"excludes": "description_hash",
},
"description_hash": {
"type": "string",
"empty": False,
"required": True,
"excludes": "memo",
},
"lnurl_callback": {"type": "string", "nullable": True, "required": False}, "lnurl_callback": {"type": "string", "nullable": True, "required": False},
"extra": {"type": "dict", "nullable": True, "required": False}, "extra": {"type": "dict", "nullable": True, "required": False},
"webhook": {"type": "string", "empty": False, "required": False}, "webhook": {"type": "string", "empty": False, "required": False},
@ -108,10 +118,14 @@ async def api_payments_create_invoice():
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
@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 = await 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:
@ -147,8 +161,18 @@ async def api_payments_create():
"description_hash": {"type": "string", "empty": False, "required": True}, "description_hash": {"type": "string", "empty": False, "required": True},
"callback": {"type": "string", "empty": False, "required": True}, "callback": {"type": "string", "empty": False, "required": True},
"amount": {"type": "number", "empty": False, "required": True}, "amount": {"type": "number", "empty": False, "required": True},
"comment": {"type": "string", "nullable": True, "empty": True, "required": False}, "comment": {
"description": {"type": "string", "nullable": True, "empty": True, "required": False}, "type": "string",
"nullable": True,
"empty": True,
"required": False,
},
"description": {
"type": "string",
"nullable": True,
"empty": True,
"required": False,
},
} }
) )
async def api_payments_pay_lnurl(): async def api_payments_pay_lnurl():
@ -168,7 +192,10 @@ async def api_payments_pay_lnurl():
params = json.loads(r.text) params = json.loads(r.text)
if params.get("status") == "ERROR": if params.get("status") == "ERROR":
return jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}), HTTPStatus.BAD_REQUEST return (
jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}),
HTTPStatus.BAD_REQUEST,
)
invoice = bolt11.decode(params["pr"]) invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != g.data["amount"]: if invoice.amount_msat != g.data["amount"]:
@ -236,7 +263,10 @@ async def api_payment(payment_hash):
except Exception: except Exception:
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK
return jsonify({"paid": not payment.pending, "preimage": payment.preimage}), HTTPStatus.OK return (
jsonify({"paid": not payment.pending, "preimage": payment.preimage}),
HTTPStatus.OK,
)
@core_app.route("/api/v1/payments/sse", methods=["GET"]) @core_app.route("/api/v1/payments/sse", methods=["GET"])
@ -325,12 +355,22 @@ async def api_lnurlscan(code: str):
data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata) data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata)
except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException): except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException):
return ( return (
jsonify({"domain": domain, "message": f"got invalid response '{r.text[:200]}'"}), jsonify(
{
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
}
),
HTTPStatus.SERVICE_UNAVAILABLE, HTTPStatus.SERVICE_UNAVAILABLE,
) )
if type(data) is lnurl.LnurlChannelResponse: if type(data) is lnurl.LnurlChannelResponse:
return jsonify({"domain": domain, "kind": "channel", "message": "unsupported"}), HTTPStatus.BAD_REQUEST return (
jsonify(
{"domain": domain, "kind": "channel", "message": "unsupported"}
),
HTTPStatus.BAD_REQUEST,
)
params.update(**data.dict()) params.update(**data.dict())

View File

@ -2,7 +2,15 @@ import trio # type: ignore
import httpx import httpx
from os import path from os import path
from http import HTTPStatus from http import HTTPStatus
from quart import g, abort, redirect, request, render_template, send_from_directory, url_for from quart import (
g,
abort,
redirect,
request,
render_template,
send_from_directory,
url_for,
)
from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl # type: ignore from lnurl import LnurlResponse, LnurlWithdrawResponse, decode as decode_lnurl # type: ignore
from lnbits.core import core_app from lnbits.core import core_app
@ -22,12 +30,16 @@ from ..services import redeem_lnurl_withdraw
@core_app.route("/favicon.ico") @core_app.route("/favicon.ico")
async def favicon(): async def favicon():
return await send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico") return await send_from_directory(
path.join(core_app.root_path, "static"), "favicon.ico"
)
@core_app.route("/") @core_app.route("/")
async def home(): async def home():
return await render_template("core/index.html", lnurl=request.args.get("lightning", None)) return await render_template(
"core/index.html", lnurl=request.args.get("lightning", None)
)
@core_app.route("/extensions") @core_app.route("/extensions")
@ -38,12 +50,18 @@ async def extensions():
extension_to_disable = request.args.get("disable", type=str) extension_to_disable = request.args.get("disable", type=str)
if extension_to_enable and extension_to_disable: if extension_to_enable and extension_to_disable:
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:
await 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:
await 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=await get_user(g.user.id)) return await render_template("core/extensions.html", user=await get_user(g.user.id))
@ -85,7 +103,9 @@ async def wallet():
if not wallet: if not wallet:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.") abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
return await render_template("core/wallet.html", user=user, wallet=wallet, service_fee=service_fee) return await render_template(
"core/wallet.html", user=user, wallet=wallet, service_fee=service_fee
)
@core_app.route("/deletewallet") @core_app.route("/deletewallet")
@ -116,19 +136,33 @@ async def lnurlwallet():
withdraw_res = LnurlResponse.from_dict(r.json()) withdraw_res = LnurlResponse.from_dict(r.json())
if not withdraw_res.ok: if not withdraw_res.ok:
return f"Could not process lnurl-withdraw: {withdraw_res.error_msg}", HTTPStatus.BAD_REQUEST return (
f"Could not process lnurl-withdraw: {withdraw_res.error_msg}",
HTTPStatus.BAD_REQUEST,
)
if not isinstance(withdraw_res, LnurlWithdrawResponse): if not isinstance(withdraw_res, LnurlWithdrawResponse):
return f"Expected an lnurl-withdraw code, got {withdraw_res.tag}", HTTPStatus.BAD_REQUEST return (
f"Expected an lnurl-withdraw code, got {withdraw_res.tag}",
HTTPStatus.BAD_REQUEST,
)
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 = await create_account() account = await create_account()
user = await get_user(account.id) user = await get_user(account.id)
wallet = await create_wallet(user_id=user.id) wallet = await create_wallet(user_id=user.id)
await db.commit() await db.commit()
g.nursery.start_soon(redeem_lnurl_withdraw, wallet.id, withdraw_res, "LNbits initial funding: voucher redeem.") g.nursery.start_soon(
redeem_lnurl_withdraw,
wallet.id,
withdraw_res,
"LNbits initial funding: voucher redeem.",
)
await trio.sleep(3) await trio.sleep(3)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id)) return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))

View File

@ -18,7 +18,9 @@ class Database:
def session_connection(self) -> Tuple[Optional[Any], Optional[Any]]: def session_connection(self) -> Tuple[Optional[Any], Optional[Any]]:
try: try:
return getattr(g, f"{self.db_name}_conn", None), getattr(g, f"{self.db_name}_txn", None) return getattr(g, f"{self.db_name}_conn", None), getattr(
g, f"{self.db_name}_txn", None
)
except RuntimeError: except RuntimeError:
return None, None return None, None

View File

@ -77,11 +77,15 @@ def check_user_exists(param: str = "usr"):
return wrap return wrap
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4): def validate_uuids(
params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4
):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
async def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
query_params = {param: request.args.get(param, type=str) for param in params} query_params = {
param: request.args.get(param, type=str) for param in params
}
for param, value in query_params.items(): for param, value in query_params.items():
if not value and (required is True or (required and param in required)): if not value and (required is True or (required and param in required)):

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_amilk") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -31,7 +31,9 @@ async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]

View File

@ -21,7 +21,10 @@ async def api_amilks():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([amilk._asdict() for amilk in await 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"])
@ -35,12 +38,18 @@ async def api_amilkit(amilk_id):
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
payment_hash, payment_request = await 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"},
) )
r = httpx.get( r = httpx.get(
withdraw_res.callback.base, withdraw_res.callback.base,
params={**withdraw_res.callback.query_params, **{"k1": withdraw_res.k1, "pr": payment_request}}, params={
**withdraw_res.callback.query_params,
**{"k1": withdraw_res.k1, "pr": payment_request},
},
) )
if r.is_error: if r.is_error:
@ -68,7 +77,10 @@ async def api_amilkit(amilk_id):
) )
async def api_amilk_create(): async def api_amilk_create():
amilk = await create_amilk( amilk = await create_amilk(
wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"] 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

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_bleskomat") db = Database("ext_bleskomat")
bleskomat_ext: Blueprint = Blueprint("bleskomat", __name__, static_folder="static", template_folder="templates") bleskomat_ext: Blueprint = Blueprint(
"bleskomat", __name__, static_folder="static", template_folder="templates"
)
from .lnurl_api import * # noqa from .lnurl_api import * # noqa
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -8,7 +8,12 @@ from .helpers import generate_bleskomat_lnurl_hash
async def create_bleskomat( async def create_bleskomat(
*, wallet_id: str, name: str, fiat_currency: str, exchange_rate_provider: str, fee: str *,
wallet_id: str,
name: str,
fiat_currency: str,
exchange_rate_provider: str,
fee: str,
) -> Bleskomat: ) -> Bleskomat:
bleskomat_id = uuid4().hex bleskomat_id = uuid4().hex
api_key_id = secrets.token_hex(8) api_key_id = secrets.token_hex(8)
@ -42,7 +47,9 @@ async def get_bleskomat(bleskomat_id: str) -> Optional[Bleskomat]:
async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]: async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]:
row = await db.fetchone("SELECT * FROM bleskomats WHERE api_key_id = ?", (api_key_id,)) row = await db.fetchone(
"SELECT * FROM bleskomats WHERE api_key_id = ?", (api_key_id,)
)
return Bleskomat(**row) if row else None return Bleskomat(**row) if row else None
@ -50,13 +57,17 @@ async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(f"SELECT * FROM bleskomats WHERE wallet IN ({q})", (*wallet_ids,)) rows = await db.fetchall(
f"SELECT * FROM bleskomats WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Bleskomat(**row) for row in rows] return [Bleskomat(**row) for row in rows]
async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]: async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]:
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 bleskomats SET {q} WHERE id = ?", (*kwargs.values(), bleskomat_id)) await db.execute(
f"UPDATE bleskomats SET {q} WHERE id = ?", (*kwargs.values(), bleskomat_id)
)
row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,)) row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,))
return Bleskomat(**row) if row else None return Bleskomat(**row) if row else None

View File

@ -3,7 +3,12 @@ import json
import os import os
fiat_currencies = json.load( fiat_currencies = json.load(
open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "fiat_currencies.json"), "r") open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)), "fiat_currencies.json"
),
"r",
)
) )
exchange_rate_providers = { exchange_rate_providers = {
@ -35,7 +40,9 @@ exchange_rate_providers = {
"name": "Kraken", "name": "Kraken",
"domain": "kraken.com", "domain": "kraken.com",
"api_url": "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}", "api_url": "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
"getter": lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0], "getter": lambda data, replacements: data["result"][
"XXBTZ" + replacements["TO"]
]["c"][0],
}, },
} }
@ -50,7 +57,12 @@ for ref, exchange_rate_provider in exchange_rate_providers.items():
async def fetch_fiat_exchange_rate(currency: str, provider: str): async def fetch_fiat_exchange_rate(currency: str, provider: str):
replacements = {"FROM": "BTC", "from": "btc", "TO": currency.upper(), "to": currency.lower()} replacements = {
"FROM": "BTC",
"from": "btc",
"TO": currency.upper(),
"to": currency.lower(),
}
url = exchange_rate_providers[provider]["api_url"] url = exchange_rate_providers[provider]["api_url"]
for key in replacements.keys(): for key in replacements.keys():

View File

@ -14,7 +14,9 @@ def generate_bleskomat_lnurl_hash(secret: str):
return m.hexdigest() return m.hexdigest()
def generate_bleskomat_lnurl_signature(payload: str, api_key_secret: str, api_key_encoding: str = "hex"): def generate_bleskomat_lnurl_signature(
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
):
if api_key_encoding == "hex": if api_key_encoding == "hex":
key = unhexlify(api_key_secret) key = unhexlify(api_key_secret)
elif api_key_encoding == "base64": elif api_key_encoding == "base64":
@ -41,7 +43,11 @@ def is_supported_lnurl_subprotocol(tag: str) -> bool:
class LnurlHttpError(Exception): class LnurlHttpError(Exception):
def __init__(self, message: str = "", http_status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR): def __init__(
self,
message: str = "",
http_status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
):
self.message = message self.message = message
self.http_status = http_status self.http_status = http_status
super().__init__(self.message) super().__init__(self.message)
@ -62,11 +68,15 @@ def prepare_lnurl_params(tag: str, query: Dict[str, str]):
if not params["minWithdrawable"] > 0: if not params["minWithdrawable"] > 0:
raise LnurlValidationError('"minWithdrawable" must be greater than zero') raise LnurlValidationError('"minWithdrawable" must be greater than zero')
if not params["maxWithdrawable"] >= params["minWithdrawable"]: if not params["maxWithdrawable"] >= params["minWithdrawable"]:
raise LnurlValidationError('"maxWithdrawable" must be greater than or equal to "minWithdrawable"') raise LnurlValidationError(
'"maxWithdrawable" must be greater than or equal to "minWithdrawable"'
)
return params return params
encode_uri_component_safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()" encode_uri_component_safe_chars = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()"
)
def query_to_signing_payload(query: Dict[str, str]) -> str: def query_to_signing_payload(query: Dict[str, str]) -> str:
@ -76,19 +86,30 @@ def query_to_signing_payload(query: Dict[str, str]) -> str:
for key in sorted_keys: for key in sorted_keys:
if not key == "signature": if not key == "signature":
encoded_key = urllib.parse.quote(key, safe=encode_uri_component_safe_chars) encoded_key = urllib.parse.quote(key, safe=encode_uri_component_safe_chars)
encoded_value = urllib.parse.quote(query[key], safe=encode_uri_component_safe_chars) encoded_value = urllib.parse.quote(
query[key], safe=encode_uri_component_safe_chars
)
payload.append(f"{encoded_key}={encoded_value}") payload.append(f"{encoded_key}={encoded_value}")
return "&".join(payload) return "&".join(payload)
unshorten_rules = { unshorten_rules = {
"query": {"n": "nonce", "s": "signature", "t": "tag"}, "query": {"n": "nonce", "s": "signature", "t": "tag"},
"tags": {"c": "channelRequest", "l": "login", "p": "payRequest", "w": "withdrawRequest"}, "tags": {
"c": "channelRequest",
"l": "login",
"p": "payRequest",
"w": "withdrawRequest",
},
"params": { "params": {
"channelRequest": {"pl": "localAmt", "pp": "pushAmt"}, "channelRequest": {"pl": "localAmt", "pp": "pushAmt"},
"login": {}, "login": {},
"payRequest": {"pn": "minSendable", "px": "maxSendable", "pm": "metadata"}, "payRequest": {"pn": "minSendable", "px": "maxSendable", "pm": "metadata"},
"withdrawRequest": {"pn": "minWithdrawable", "px": "maxWithdrawable", "pd": "defaultDescription"}, "withdrawRequest": {
"pn": "minWithdrawable",
"px": "maxWithdrawable",
"pd": "defaultDescription",
},
}, },
} }

View File

@ -47,7 +47,10 @@ async def api_bleskomat_lnurl():
# The API key ID, nonce, and tag should be present in the query string. # The API key ID, nonce, and tag should be present in the query string.
for field in ["id", "nonce", "tag"]: for field in ["id", "nonce", "tag"]:
if not field in query: if not field in query:
raise LnurlHttpError(f'Failed API key signature check: Missing "{field}"', HTTPStatus.BAD_REQUEST) raise LnurlHttpError(
f'Failed API key signature check: Missing "{field}"',
HTTPStatus.BAD_REQUEST,
)
# URL signing scheme is described here: # URL signing scheme is described here:
# https://github.com/chill117/lnurl-node#how-to-implement-url-signing-scheme # https://github.com/chill117/lnurl-node#how-to-implement-url-signing-scheme
@ -58,7 +61,9 @@ async def api_bleskomat_lnurl():
raise LnurlHttpError("Unknown API key", HTTPStatus.BAD_REQUEST) raise LnurlHttpError("Unknown API key", HTTPStatus.BAD_REQUEST)
api_key_secret = bleskomat.api_key_secret api_key_secret = bleskomat.api_key_secret
api_key_encoding = bleskomat.api_key_encoding api_key_encoding = bleskomat.api_key_encoding
expected_signature = generate_bleskomat_lnurl_signature(payload, api_key_secret, api_key_encoding) expected_signature = generate_bleskomat_lnurl_signature(
payload, api_key_secret, api_key_encoding
)
if signature != expected_signature: if signature != expected_signature:
raise LnurlHttpError("Invalid API key signature", HTTPStatus.FORBIDDEN) raise LnurlHttpError("Invalid API key signature", HTTPStatus.FORBIDDEN)
@ -72,13 +77,16 @@ async def api_bleskomat_lnurl():
params = prepare_lnurl_params(tag, query) params = prepare_lnurl_params(tag, query)
if "f" in query: if "f" in query:
rate = await fetch_fiat_exchange_rate( rate = await fetch_fiat_exchange_rate(
currency=query["f"], provider=bleskomat.exchange_rate_provider currency=query["f"],
provider=bleskomat.exchange_rate_provider,
) )
# Convert fee (%) to decimal: # Convert fee (%) to decimal:
fee = float(bleskomat.fee) / 100 fee = float(bleskomat.fee) / 100
if tag == "withdrawRequest": if tag == "withdrawRequest":
for key in ["minWithdrawable", "maxWithdrawable"]: for key in ["minWithdrawable", "maxWithdrawable"]:
amount_sats = int(math.floor((params[key] / rate) * 1e8)) amount_sats = int(
math.floor((params[key] / rate) * 1e8)
)
fee_sats = int(math.floor(amount_sats * fee)) fee_sats = int(math.floor(amount_sats * fee))
amount_sats_less_fee = amount_sats - fee_sats amount_sats_less_fee = amount_sats - fee_sats
# Convert to msats: # Convert to msats:
@ -87,7 +95,9 @@ async def api_bleskomat_lnurl():
raise LnurlHttpError(e.message, HTTPStatus.BAD_REQUEST) raise LnurlHttpError(e.message, HTTPStatus.BAD_REQUEST)
# Create a new LNURL using the query parameters provided in the signed URL. # Create a new LNURL using the query parameters provided in the signed URL.
params = json.JSONEncoder().encode(params) params = json.JSONEncoder().encode(params)
lnurl = await create_bleskomat_lnurl(bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1) lnurl = await create_bleskomat_lnurl(
bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1
)
# Reply with LNURL response object. # Reply with LNURL response object.
return jsonify(lnurl.get_info_response_object(secret)), HTTPStatus.OK return jsonify(lnurl.get_info_response_object(secret)), HTTPStatus.OK
@ -104,7 +114,9 @@ async def api_bleskomat_lnurl():
raise LnurlHttpError("Invalid secret", HTTPStatus.BAD_REQUEST) raise LnurlHttpError("Invalid secret", HTTPStatus.BAD_REQUEST)
if not lnurl.has_uses_remaining(): if not lnurl.has_uses_remaining():
raise LnurlHttpError("Maximum number of uses already reached", HTTPStatus.BAD_REQUEST) raise LnurlHttpError(
"Maximum number of uses already reached", HTTPStatus.BAD_REQUEST
)
try: try:
await lnurl.execute_action(query) await lnurl.execute_action(query)
@ -115,6 +127,9 @@ async def api_bleskomat_lnurl():
return jsonify({"status": "ERROR", "reason": str(e)}), e.http_status return jsonify({"status": "ERROR", "reason": str(e)}), e.http_status
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return jsonify({"status": "ERROR", "reason": "Unexpected error"}), HTTPStatus.INTERNAL_SERVER_ERROR return (
jsonify({"status": "ERROR", "reason": "Unexpected error"}),
HTTPStatus.INTERNAL_SERVER_ERROR,
)
return jsonify({"status": "OK"}), HTTPStatus.OK return jsonify({"status": "OK"}), HTTPStatus.OK

View File

@ -62,11 +62,17 @@ class BleskomatLnurl(NamedTuple):
try: try:
invoice = bolt11.decode(pr) invoice = bolt11.decode(pr)
except ValueError as e: except ValueError as e:
raise LnurlValidationError('Invalid parameter ("pr"): Lightning payment request expected') raise LnurlValidationError(
'Invalid parameter ("pr"): Lightning payment request expected'
)
if invoice.amount_msat < params["minWithdrawable"]: if invoice.amount_msat < params["minWithdrawable"]:
raise LnurlValidationError('Amount in invoice must be greater than or equal to "minWithdrawable"') raise LnurlValidationError(
'Amount in invoice must be greater than or equal to "minWithdrawable"'
)
if invoice.amount_msat > params["maxWithdrawable"]: if invoice.amount_msat > params["maxWithdrawable"]:
raise LnurlValidationError('Amount in invoice must be less than or equal to "maxWithdrawable"') raise LnurlValidationError(
'Amount in invoice must be less than or equal to "maxWithdrawable"'
)
else: else:
raise LnurlValidationError(f'Unknown subprotocol: "{tag}"') raise LnurlValidationError(f'Unknown subprotocol: "{tag}"')

View File

@ -17,4 +17,6 @@ async def index():
"exchange_rate_providers": exchange_rate_providers_serializable, "exchange_rate_providers": exchange_rate_providers_serializable,
"fiat_currencies": fiat_currencies, "fiat_currencies": fiat_currencies,
} }
return await render_template("bleskomat/index.html", user=g.user, bleskomat_vars=bleskomat_vars) return await render_template(
"bleskomat/index.html", user=g.user, bleskomat_vars=bleskomat_vars
)

View File

@ -28,7 +28,12 @@ async def api_bleskomats():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([bleskomat._asdict() for bleskomat in await get_bleskomats(wallet_ids)]), HTTPStatus.OK return (
jsonify(
[bleskomat._asdict() for bleskomat in await get_bleskomats(wallet_ids)]
),
HTTPStatus.OK,
)
@bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["GET"]) @bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["GET"])
@ -37,7 +42,10 @@ async def api_bleskomat_retrieve(bleskomat_id):
bleskomat = await get_bleskomat(bleskomat_id) bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id: if not bleskomat or bleskomat.wallet != g.wallet.id:
return jsonify({"message": "Bleskomat configuration not found."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
)
return jsonify(bleskomat._asdict()), HTTPStatus.OK return jsonify(bleskomat._asdict()), HTTPStatus.OK
@ -48,8 +56,16 @@ async def api_bleskomat_retrieve(bleskomat_id):
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"name": {"type": "string", "empty": False, "required": True}, "name": {"type": "string", "empty": False, "required": True},
"fiat_currency": {"type": "string", "allowed": fiat_currencies.keys(), "required": True}, "fiat_currency": {
"exchange_rate_provider": {"type": "string", "allowed": exchange_rate_providers.keys(), "required": True}, "type": "string",
"allowed": fiat_currencies.keys(),
"required": True,
},
"exchange_rate_provider": {
"type": "string",
"allowed": exchange_rate_providers.keys(),
"required": True,
},
"fee": {"type": ["string", "float", "number", "integer"], "required": True}, "fee": {"type": ["string", "float", "number", "integer"], "required": True},
} }
) )
@ -58,23 +74,35 @@ async def api_bleskomat_create_or_update(bleskomat_id=None):
try: try:
fiat_currency = g.data["fiat_currency"] fiat_currency = g.data["fiat_currency"]
exchange_rate_provider = g.data["exchange_rate_provider"] exchange_rate_provider = g.data["exchange_rate_provider"]
rate = await fetch_fiat_exchange_rate(currency=fiat_currency, provider=exchange_rate_provider) rate = await fetch_fiat_exchange_rate(
currency=fiat_currency, provider=exchange_rate_provider
)
except Exception as e: except Exception as e:
print(e) print(e)
return ( return (
jsonify({"message": f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"'}), jsonify(
{
"message": f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"'
}
),
HTTPStatus.INTERNAL_SERVER_ERROR, HTTPStatus.INTERNAL_SERVER_ERROR,
) )
if bleskomat_id: if bleskomat_id:
bleskomat = await get_bleskomat(bleskomat_id) bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id: if not bleskomat or bleskomat.wallet != g.wallet.id:
return jsonify({"message": "Bleskomat configuration not found."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
)
bleskomat = await update_bleskomat(bleskomat_id, **g.data) bleskomat = await update_bleskomat(bleskomat_id, **g.data)
else: else:
bleskomat = await create_bleskomat(wallet_id=g.wallet.id, **g.data) bleskomat = await create_bleskomat(wallet_id=g.wallet.id, **g.data)
return jsonify(bleskomat._asdict()), HTTPStatus.OK if bleskomat_id else HTTPStatus.CREATED return (
jsonify(bleskomat._asdict()),
HTTPStatus.OK if bleskomat_id else HTTPStatus.CREATED,
)
@bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["DELETE"]) @bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["DELETE"])
@ -83,7 +111,10 @@ async def api_bleskomat_delete(bleskomat_id):
bleskomat = await get_bleskomat(bleskomat_id) bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id: if not bleskomat or bleskomat.wallet != g.wallet.id:
return jsonify({"message": "Bleskomat configuration not found."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
)
await delete_bleskomat(bleskomat_id) await delete_bleskomat(bleskomat_id)

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_captcha") db = Database("ext_captcha")
captcha_ext: Blueprint = Blueprint("captcha", __name__, static_folder="static", template_folder="templates") captcha_ext: Blueprint = Blueprint(
"captcha", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -7,7 +7,13 @@ from .models import Captcha
async def create_captcha( async def create_captcha(
*, 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,
) -> Captcha: ) -> Captcha:
captcha_id = urlsafe_short_hash() captcha_id = urlsafe_short_hash()
await db.execute( await db.execute(
@ -34,7 +40,9 @@ async def get_captchas(wallet_ids: Union[str, List[str]]) -> List[Captcha]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(f"SELECT * FROM captchas WHERE wallet IN ({q})", (*wallet_ids,)) rows = await db.fetchall(
f"SELECT * FROM captchas WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Captcha.from_row(row) for row in rows] return [Captcha.from_row(row) for row in rows]

View File

@ -46,7 +46,9 @@ async def m002_redux(db):
) )
await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON captchas (wallet)") await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON captchas (wallet)")
for row in [list(row) for row in await db.fetchall("SELECT * FROM captchas_old")]: for row in [
list(row) for row in await db.fetchall("SELECT * FROM captchas_old")
]:
await db.execute( await db.execute(
""" """
INSERT INTO captchas ( INSERT INTO captchas (

View File

@ -16,5 +16,7 @@ async def index():
@captcha_ext.route("/<captcha_id>") @captcha_ext.route("/<captcha_id>")
async def display(captcha_id): async def display(captcha_id):
captcha = await get_captcha(captcha_id) or abort(HTTPStatus.NOT_FOUND, "captcha does not exist.") captcha = await get_captcha(captcha_id) or abort(
HTTPStatus.NOT_FOUND, "captcha does not exist."
)
return await render_template("captcha/display.html", captcha=captcha) return await render_template("captcha/display.html", captcha=captcha)

View File

@ -17,7 +17,10 @@ async def api_captchas():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([captcha._asdict() for captcha in await get_captchas(wallet_ids)]), HTTPStatus.OK return (
jsonify([captcha._asdict() for captcha in await get_captchas(wallet_ids)]),
HTTPStatus.OK,
)
@captcha_ext.route("/api/v1/captchas", methods=["POST"]) @captcha_ext.route("/api/v1/captchas", methods=["POST"])
@ -26,7 +29,12 @@ async def api_captchas():
schema={ schema={
"url": {"type": "string", "empty": False, "required": True}, "url": {"type": "string", "empty": False, "required": True},
"memo": {"type": "string", "empty": False, "required": True}, "memo": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "empty": True, "nullable": True, "required": False}, "description": {
"type": "string",
"empty": True,
"nullable": True,
"required": False,
},
"amount": {"type": "integer", "min": 0, "required": True}, "amount": {"type": "integer", "min": 0, "required": True},
"remembers": {"type": "boolean", "required": True}, "remembers": {"type": "boolean", "required": True},
} }
@ -53,26 +61,41 @@ async def api_captcha_delete(captcha_id):
@captcha_ext.route("/api/v1/captchas/<captcha_id>/invoice", methods=["POST"]) @captcha_ext.route("/api/v1/captchas/<captcha_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_captcha_create_invoice(captcha_id): async def api_captcha_create_invoice(captcha_id):
captcha = await get_captcha(captcha_id) captcha = await get_captcha(captcha_id)
if g.data["amount"] < captcha.amount: if g.data["amount"] < captcha.amount:
return jsonify({"message": f"Minimum amount is {captcha.amount} sat."}), HTTPStatus.BAD_REQUEST return (
jsonify({"message": f"Minimum amount is {captcha.amount} sat."}),
HTTPStatus.BAD_REQUEST,
)
try: try:
amount = g.data["amount"] if g.data["amount"] > captcha.amount else captcha.amount amount = (
g.data["amount"] if g.data["amount"] > captcha.amount else captcha.amount
)
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=captcha.wallet, amount=amount, memo=f"{captcha.memo}", extra={"tag": "captcha"} wallet_id=captcha.wallet,
amount=amount,
memo=f"{captcha.memo}",
extra={"tag": "captcha"},
) )
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
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.CREATED,
)
@captcha_ext.route("/api/v1/captchas/<captcha_id>/check_invoice", methods=["POST"]) @captcha_ext.route("/api/v1/captchas/<captcha_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(captcha_id): async def api_paywal_check_invoice(captcha_id):
captcha = await get_captcha(captcha_id) captcha = await get_captcha(captcha_id)
@ -90,6 +113,9 @@ async def api_paywal_check_invoice(captcha_id):
payment = await wallet.get_payment(g.data["payment_hash"]) payment = await wallet.get_payment(g.data["payment_hash"])
await payment.set_pending(False) await payment.set_pending(False)
return jsonify({"paid": True, "url": captcha.url, "remembers": captcha.remembers}), HTTPStatus.OK return (
jsonify({"paid": True, "url": captcha.url, "remembers": captcha.remembers}),
HTTPStatus.OK,
)
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK

View File

@ -1,7 +1,9 @@
from quart import Blueprint from quart import Blueprint
diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates") diagonalley_ext: Blueprint = Blueprint(
"diagonalley", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -21,7 +21,14 @@ regex = re.compile(
def create_diagonalleys_product( def create_diagonalleys_product(
*, wallet_id: str, product: str, categories: str, description: str, image: str, price: int, quantity: int *,
wallet_id: str,
product: str,
categories: str,
description: str,
image: str,
price: int,
quantity: int,
) -> Products: ) -> Products:
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
@ -30,7 +37,16 @@ def create_diagonalleys_product(
INSERT INTO products (id, wallet, product, categories, description, image, price, quantity) INSERT INTO products (id, wallet, product, categories, description, image, price, quantity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(product_id, wallet_id, product, categories, description, image, price, quantity), (
product_id,
wallet_id,
product,
categories,
description,
image,
price,
quantity,
),
) )
return get_diagonalleys_product(product_id) return get_diagonalleys_product(product_id)
@ -40,7 +56,9 @@ def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
db.execute(f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id)) db.execute(
f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id)
)
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,)) row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
return get_diagonalleys_indexer(product_id) return get_diagonalleys_indexer(product_id)
@ -59,7 +77,9 @@ def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Product
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,)) rows = db.fetchall(
f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Products(**row) for row in rows] return [Products(**row) for row in rows]
@ -110,7 +130,9 @@ def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
db.execute(f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id)) db.execute(
f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id)
)
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
return get_diagonalleys_indexer(indexer_id) return get_diagonalleys_indexer(indexer_id)
@ -154,7 +176,9 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)) rows = db.fetchall(
f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows: for r in rows:
try: try:
@ -181,7 +205,9 @@ def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexer
print("An exception occurred") print("An exception occurred")
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)) rows = db.fetchall(
f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Indexers(**row) for row in rows] return [Indexers(**row) for row in rows]
@ -213,7 +239,19 @@ def create_diagonalleys_order(
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped) INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(order_id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, False, False), (
order_id,
productid,
wallet,
product,
quantity,
shippingzone,
address,
email,
invoiceid,
False,
False,
),
) )
return get_diagonalleys_order(order_id) return get_diagonalleys_order(order_id)
@ -232,7 +270,9 @@ def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)) rows = db.fetchall(
f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows: for r in rows:
PAID = WALLET.get_invoice_status(r["invoiceid"]).paid PAID = WALLET.get_invoice_status(r["invoiceid"]).paid
if PAID: if PAID:
@ -244,7 +284,9 @@ def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
r["id"], r["id"],
), ),
) )
rows = db.fetchall(f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)) rows = db.fetchall(
f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Orders(**row) for row in rows] return [Orders(**row) for row in rows]

View File

@ -36,7 +36,12 @@ async def api_diagonalley_products():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([product._asdict() for product in get_diagonalleys_products(wallet_ids)]), HTTPStatus.OK return (
jsonify(
[product._asdict() for product in get_diagonalleys_products(wallet_ids)]
),
HTTPStatus.OK,
)
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"]) @diagonalley_ext.route("/api/v1/diagonalley/products", methods=["POST"])
@ -58,16 +63,25 @@ async def api_diagonalley_product_create(product_id=None):
product = get_diagonalleys_indexer(product_id) product = get_diagonalleys_indexer(product_id)
if not product: if not product:
return jsonify({"message": "Withdraw product does not exist."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "Withdraw product does not exist."}),
HTTPStatus.NOT_FOUND,
)
if product.wallet != g.wallet.id: if product.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw product."}), HTTPStatus.FORBIDDEN return (
jsonify({"message": "Not your withdraw product."}),
HTTPStatus.FORBIDDEN,
)
product = update_diagonalleys_product(product_id, **g.data) product = update_diagonalleys_product(product_id, **g.data)
else: else:
product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data) product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data)
return jsonify(product._asdict()), HTTPStatus.OK if product_id else HTTPStatus.CREATED return (
jsonify(product._asdict()),
HTTPStatus.OK if product_id else HTTPStatus.CREATED,
)
@diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"]) @diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"])
@ -97,7 +111,12 @@ async def api_diagonalley_indexers():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]), HTTPStatus.OK return (
jsonify(
[indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)]
),
HTTPStatus.OK,
)
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"]) @diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["POST"])
@ -120,16 +139,25 @@ async def api_diagonalley_indexer_create(indexer_id=None):
indexer = get_diagonalleys_indexer(indexer_id) indexer = get_diagonalleys_indexer(indexer_id)
if not indexer: if not indexer:
return jsonify({"message": "Withdraw indexer does not exist."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "Withdraw indexer does not exist."}),
HTTPStatus.NOT_FOUND,
)
if indexer.wallet != g.wallet.id: if indexer.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw indexer."}), HTTPStatus.FORBIDDEN return (
jsonify({"message": "Not your withdraw indexer."}),
HTTPStatus.FORBIDDEN,
)
indexer = update_diagonalleys_indexer(indexer_id, **g.data) indexer = update_diagonalleys_indexer(indexer_id, **g.data)
else: else:
indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data) indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **g.data)
return jsonify(indexer._asdict()), HTTPStatus.OK if indexer_id else HTTPStatus.CREATED return (
jsonify(indexer._asdict()),
HTTPStatus.OK if indexer_id else HTTPStatus.CREATED,
)
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"]) @diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
@ -159,7 +187,10 @@ async def api_diagonalley_orders():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids wallet_ids = get_user(g.wallet.user).wallet_ids
return jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]), HTTPStatus.OK return (
jsonify([order._asdict() for order in get_diagonalleys_orders(wallet_ids)]),
HTTPStatus.OK,
)
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"]) @diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["POST"])
@ -221,13 +252,20 @@ async def api_diagonalleys_order_shipped(order_id):
) )
order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,)) order = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return jsonify([order._asdict() for order in get_diagonalleys_orders(order["wallet"])]), HTTPStatus.OK return (
jsonify(
[order._asdict() for order in get_diagonalleys_orders(order["wallet"])]
),
HTTPStatus.OK,
)
###List products based on indexer id ###List products based on indexer id
@diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"]) @diagonalley_ext.route(
"/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"]
)
async def api_diagonalleys_stall_products(indexer_id): async def api_diagonalleys_stall_products(indexer_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
@ -239,13 +277,20 @@ async def api_diagonalleys_stall_products(indexer_id):
if not products: if not products:
return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND return jsonify({"message": "No products"}), HTTPStatus.NOT_FOUND
return jsonify([products._asdict() for products in get_diagonalleys_products(rows[1])]), HTTPStatus.OK return (
jsonify(
[products._asdict() for products in get_diagonalleys_products(rows[1])]
),
HTTPStatus.OK,
)
###Check a product has been shipped ###Check a product has been shipped
@diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"]) @diagonalley_ext.route(
"/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"]
)
async def api_diagonalleys_stall_checkshipped(checking_id): async def api_diagonalleys_stall_checkshipped(checking_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,)) rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
@ -276,7 +321,9 @@ async def api_diagonalley_stall_order(indexer_id):
shippingcost = shipping.zone2cost shippingcost = shipping.zone2cost
checking_id, payment_request = create_invoice( checking_id, payment_request = create_invoice(
wallet_id=product.wallet, amount=shippingcost + (g.data["quantity"] * product.price), memo=g.data["id"] wallet_id=product.wallet,
amount=shippingcost + (g.data["quantity"] * product.price),
memo=g.data["id"],
) )
selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
@ -299,4 +346,7 @@ async def api_diagonalley_stall_order(indexer_id):
False, False,
), ),
) )
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK return (
jsonify({"checking_id": checking_id, "payment_request": payment_request}),
HTTPStatus.OK,
)

View File

@ -4,7 +4,9 @@ from lnbits.db import Database
db = Database("ext_events") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -9,7 +9,9 @@ from .models import Tickets, Events
# TICKETS # TICKETS
async 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:
await db.execute( await db.execute(
""" """
INSERT INTO ticket (id, wallet, event, name, email, registered, paid) INSERT INTO ticket (id, wallet, event, name, email, registered, paid)
@ -64,7 +66,9 @@ async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]
@ -113,7 +117,9 @@ async def create_event(
async 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()])
await db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id)) await db.execute(
f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id)
)
event = await get_event(event_id) event = await get_event(event_id)
assert event, "Newly updated event couldn't be retrieved" assert event, "Newly updated event couldn't be retrieved"
return event return event
@ -129,7 +135,9 @@ async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]
@ -142,7 +150,9 @@ async def delete_event(event_id: str) -> None:
async def get_event_tickets(event_id: str, wallet_id: str) -> List[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)) rows = await db.fetchall(
"SELECT * FROM ticket WHERE wallet = ? AND event = ?", (wallet_id, event_id)
)
return [Tickets(**row) for row in rows] return [Tickets(**row) for row in rows]

View File

@ -23,12 +23,16 @@ async def display(event_id):
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 :(",
) )
datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date() datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date()
if date.today() > datetime_object: if date.today() > datetime_object:
return await render_template( return await render_template(
"events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(" "events/error.html",
event_name=event.name,
event_error="Sorry, ticket closing date has passed :(",
) )
return await render_template( return await render_template(
@ -51,7 +55,10 @@ async def ticket(ticket_id):
abort(HTTPStatus.NOT_FOUND, "Event does not exist.") 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,
) )
@ -62,5 +69,8 @@ async def register(event_id):
abort(HTTPStatus.NOT_FOUND, "Event does not exist.") abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return await render_template( return await render_template(
"events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet "events/register.html",
event_id=event_id,
event_name=event.name,
wallet_id=event.wallet,
) )

View File

@ -33,7 +33,10 @@ async def api_events():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([event._asdict() for event in await 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"])
@ -92,7 +95,10 @@ async def api_tickets():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([ticket._asdict() for ticket in await 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"])
@ -108,17 +114,25 @@ async def api_ticket_make_ticket(event_id, sats):
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 = await 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 = await 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
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.OK,
)
@events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
@ -163,7 +177,14 @@ async def api_ticket_delete(ticket_id):
@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 await 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,
) )
@ -177,4 +198,7 @@ async def api_event_register_ticket(ticket_id):
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 await reg_ticket(ticket_id)]), HTTPStatus.OK return (
jsonify([ticket._asdict() for ticket in await reg_ticket(ticket_id)]),
HTTPStatus.OK,
)

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_example") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_lndhub") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -13,11 +13,15 @@ def check_wallet(requires_admin=False):
key_type, key = b64decode(token).decode("utf-8").split(":") key_type, key = b64decode(token).decode("utf-8").split(":")
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 = await 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)
return wrapped_view return wrapped_view

View File

@ -23,14 +23,20 @@ async def lndhub_getinfo():
schema={ schema={
"login": {"type": "string", "required": True, "excludes": "refresh_token"}, "login": {"type": "string", "required": True, "excludes": "refresh_token"},
"password": {"type": "string", "required": True, "excludes": "refresh_token"}, "password": {"type": "string", "required": True, "excludes": "refresh_token"},
"refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]}, "refresh_token": {
"type": "string",
"required": True,
"excludes": ["login", "password"],
},
} }
) )
async def lndhub_auth(): async def lndhub_auth():
token = ( token = (
g.data["refresh_token"] g.data["refresh_token"]
if "refresh_token" in g.data and g.data["refresh_token"] if "refresh_token" in g.data and g.data["refresh_token"]
else urlsafe_b64encode((g.data["login"] + ":" + g.data["password"]).encode("utf-8")).decode("ascii") else urlsafe_b64encode(
(g.data["login"] + ":" + g.data["password"]).encode("utf-8")
).decode("ascii")
) )
return jsonify({"refresh_token": token, "access_token": token}) return jsonify({"refresh_token": token, "access_token": token})
@ -120,9 +126,15 @@ async def lndhub_balance():
@check_wallet() @check_wallet()
async def lndhub_gettxs(): async def lndhub_gettxs():
for payment in await 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,
): ):
await 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(
@ -135,10 +147,16 @@ async def lndhub_gettxs():
"fee": payment.fee, "fee": payment.fee,
"value": int(payment.amount / 1000), "value": int(payment.amount / 1000),
"timestamp": payment.time, "timestamp": payment.time,
"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(
(await 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]
) )
] ]
) )
@ -149,9 +167,15 @@ async def lndhub_gettxs():
async def lndhub_getuserinvoices(): async def lndhub_getuserinvoices():
await delete_expired_invoices() await delete_expired_invoices()
for invoice in await 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,
): ):
await 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 +193,11 @@ async def lndhub_getuserinvoices():
"type": "user_invoice", "type": "user_invoice",
} }
for invoice in reversed( for invoice in reversed(
(await g.wallet.get_payments(pending=True, complete=True, incoming=True, outgoing=False))[:limit] (
await g.wallet.get_payments(
pending=True, complete=True, incoming=True, outgoing=False
)
)[:limit]
) )
] ]
) )

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_lnticket") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -60,7 +60,12 @@ async def set_ticket_paid(payment_hash: str) -> Tickets:
try: try:
r = await client.post( r = await client.post(
formdata.webhook, formdata.webhook,
json={"form": ticket.form, "name": ticket.name, "email": ticket.email, "content": ticket.ltext}, json={
"form": ticket.form,
"name": ticket.name,
"email": ticket.email,
"content": ticket.ltext,
},
timeout=40, timeout=40,
) )
except AssertionError: except AssertionError:
@ -80,7 +85,9 @@ async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]
@ -93,7 +100,12 @@ async def delete_ticket(ticket_id: str) -> None:
async def create_form( async def create_form(
*, wallet: str, name: str, webhook: Optional[str] = None, description: str, costpword: int *,
wallet: str,
name: str,
webhook: Optional[str] = None,
description: str,
costpword: int,
) -> Forms: ) -> Forms:
form_id = urlsafe_short_hash() form_id = urlsafe_short_hash()
await db.execute( await db.execute(
@ -127,7 +139,9 @@ async def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(f"SELECT * FROM form WHERE wallet IN ({q})", (*wallet_ids,)) rows = await db.fetchall(
f"SELECT * FROM form WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Forms(**row) for row in rows] return [Forms(**row) for row in rows]

View File

@ -32,7 +32,10 @@ async def api_forms():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([form._asdict() for form in await 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"])
@ -90,7 +93,10 @@ async def api_tickets():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([form._asdict() for form in await 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"])
@ -117,12 +123,20 @@ async def api_ticket_make_ticket(form_id):
extra={"tag": "lnticket"}, extra={"tag": "lnticket"},
) )
ticket = await create_ticket(payment_hash=payment_hash, wallet=form.wallet, **g.data) ticket = await create_ticket(
payment_hash=payment_hash, wallet=form.wallet, **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,
)
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.OK,
)
@lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_lnurlp") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -74,14 +74,18 @@ async def get_pay_links(wallet_ids: Union[str, List[str]]) -> List[PayLink]:
async 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)) await db.execute(
f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
)
row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,)) row = await 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
async 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)) await db.execute(
f"UPDATE pay_links SET {q} WHERE id = ?", (*kwargs.values(), link_id)
)
row = await db.fetchone("SELECT * FROM pay_links WHERE id = ?", (link_id,)) row = await 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

View File

@ -15,7 +15,10 @@ from .crud import increment_pay_link
async def api_lnurl_response(link_id): async def api_lnurl_response(link_id):
link = await 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,
)
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
resp = LnurlPayResponse( resp = LnurlPayResponse(
@ -36,7 +39,10 @@ async def api_lnurl_response(link_id):
async def api_lnurl_callback(link_id): async def api_lnurl_callback(link_id):
link = await 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,
)
min, max = link.min, link.max min, max = link.min, link.max
rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1
@ -51,12 +57,20 @@ async def api_lnurl_callback(link_id):
amount_received = int(request.args.get("amount")) amount_received = int(request.args.get("amount"))
if amount_received < min: if amount_received < min:
return ( return (
jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is smaller than minimum {min}.").dict()), jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is smaller than minimum {min}."
).dict()
),
HTTPStatus.OK, HTTPStatus.OK,
) )
elif amount_received > max: elif amount_received > max:
return ( return (
jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is greater than maximum {max}.").dict()), jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is greater than maximum {max}."
).dict()
),
HTTPStatus.OK, HTTPStatus.OK,
) )
@ -75,7 +89,9 @@ async def api_lnurl_callback(link_id):
wallet_id=link.wallet, wallet_id=link.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=link.description, memo=link.description,
description_hash=hashlib.sha256(link.lnurlpay_metadata.encode("utf-8")).digest(), description_hash=hashlib.sha256(
link.lnurlpay_metadata.encode("utf-8")
).digest(),
extra={"tag": "lnurlp", "link": link.id, "comment": comment}, extra={"tag": "lnurlp", "link": link.id, "comment": comment},
) )

View File

@ -40,8 +40,12 @@ 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.
""" """
await db.execute("ALTER TABLE pay_links ADD COLUMN currency TEXT;") # null = satoshis await db.execute(
await db.execute("ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;") "ALTER TABLE pay_links ADD COLUMN currency TEXT;"
) # null = satoshis
await db.execute(
"ALTER TABLE pay_links ADD COLUMN comment_chars INTEGER DEFAULT 0;"
)
await db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;") await db.execute("ALTER TABLE pay_links RENAME COLUMN amount TO min;")
await db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;") await db.execute("ALTER TABLE pay_links ADD COLUMN max INTEGER;")
await db.execute("UPDATE pay_links SET max = min;") await db.execute("UPDATE pay_links SET max = min;")

View File

@ -31,12 +31,21 @@ async def api_links():
try: try:
return ( return (
jsonify([{**link._asdict(), **{"lnurl": link.lnurl}} for link in await 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:
return ( return (
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}), jsonify(
{
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
}
),
HTTPStatus.UPGRADE_REQUIRED, HTTPStatus.UPGRADE_REQUIRED,
) )
@ -83,7 +92,10 @@ async def api_link_create_or_update(link_id=None):
link = await 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,
)
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
@ -92,7 +104,10 @@ async def api_link_create_or_update(link_id=None):
else: else:
link = await 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,
)
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) @lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])

View File

@ -4,7 +4,9 @@ from lnbits.db import Database
db = Database("ext_offlineshop") db = Database("ext_offlineshop")
offlineshop_ext: Blueprint = Blueprint("offlineshop", __name__, static_folder="static", template_folder="templates") offlineshop_ext: Blueprint = Blueprint(
"offlineshop", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -18,7 +18,11 @@ async def lnurl_response(item_id):
if not item.enabled: if not item.enabled:
return jsonify({"status": "ERROR", "reason": "Item disabled."}) return jsonify({"status": "ERROR", "reason": "Item disabled."})
price_msat = (await fiat_amount_as_satoshis(item.price, item.unit) if item.unit != "sat" else item.price) * 1000 price_msat = (
await fiat_amount_as_satoshis(item.price, item.unit)
if item.unit != "sat"
else item.price
) * 1000
resp = LnurlPayResponse( resp = LnurlPayResponse(
callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True), callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True),
@ -47,16 +51,26 @@ async def lnurl_callback(item_id):
amount_received = int(request.args.get("amount")) amount_received = int(request.args.get("amount"))
if amount_received < min: if amount_received < min:
return jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is smaller than minimum {min}.").dict()) return jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is smaller than minimum {min}."
).dict()
)
elif amount_received > max: elif amount_received > max:
return jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is greater than maximum {max}.").dict()) return jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is greater than maximum {max}."
).dict()
)
shop = await get_shop(item.shop) shop = await get_shop(item.shop)
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=shop.wallet, wallet_id=shop.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=item.name, memo=item.name,
description_hash=hashlib.sha256((await item.lnurlpay_metadata()).encode("utf-8")).digest(), description_hash=hashlib.sha256(
(await item.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "offlineshop", "item": item.id}, extra={"tag": "offlineshop", "item": item.id},
) )

View File

@ -89,7 +89,9 @@ class Item(NamedTuple):
@property @property
def lnurl(self) -> str: def lnurl(self) -> str:
return lnurl_encode(url_for("offlineshop.lnurl_response", item_id=self.id, _external=True)) return lnurl_encode(
url_for("offlineshop.lnurl_response", item_id=self.id, _external=True)
)
def values(self): def values(self):
values = self._asdict() values = self._asdict()
@ -104,11 +106,15 @@ class Item(NamedTuple):
return LnurlPayMetadata(json.dumps(metadata)) return LnurlPayMetadata(json.dumps(metadata))
def success_action(self, shop: Shop, payment_hash: str) -> Optional[LnurlPaySuccessAction]: def success_action(
self, shop: Shop, payment_hash: str
) -> Optional[LnurlPaySuccessAction]:
if not shop.wordlist: if not shop.wordlist:
return None return None
return UrlAction( return UrlAction(
url=url_for("offlineshop.confirmation_code", p=payment_hash, _external=True), url=url_for(
"offlineshop.confirmation_code", p=payment_hash, _external=True
),
description="Open to get the confirmation code for your purchase.", description="Open to get the confirmation code for your purchase.",
) )

View File

@ -24,7 +24,13 @@ async def print_qr_codes():
for item_id in request.args.get("items").split(","): for item_id in request.args.get("items").split(","):
item = await get_item(item_id) item = await get_item(item_id)
if item: if item:
items.append({"lnurl": item.lnurl, "name": item.name, "price": f"{item.price} {item.unit}"}) items.append(
{
"lnurl": item.lnurl,
"name": item.name,
"price": f"{item.price} {item.unit}",
}
)
return await render_template("offlineshop/print.html", items=items) return await render_template("offlineshop/print.html", items=items)
@ -36,10 +42,14 @@ async def confirmation_code():
payment_hash = request.args.get("p") payment_hash = request.args.get("p")
payment: Payment = await get_standalone_payment(payment_hash) payment: Payment = await get_standalone_payment(payment_hash)
if not payment: if not payment:
return f"Couldn't find the payment {payment_hash}." + style, HTTPStatus.NOT_FOUND return (
f"Couldn't find the payment {payment_hash}." + style,
HTTPStatus.NOT_FOUND,
)
if payment.pending: if payment.pending:
return ( return (
f"Payment {payment_hash} wasn't received yet. Please try again in a minute." + style, f"Payment {payment_hash} wasn't received yet. Please try again in a minute."
+ style,
HTTPStatus.PAYMENT_REQUIRED, HTTPStatus.PAYMENT_REQUIRED,
) )

View File

@ -43,7 +43,11 @@ async def api_shop_from_wallet():
) )
except LnurlInvalidUrl: except LnurlInvalidUrl:
return ( return (
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}), jsonify(
{
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
}
),
HTTPStatus.UPGRADE_REQUIRED, HTTPStatus.UPGRADE_REQUIRED,
) )
@ -98,7 +102,12 @@ async def api_delete_item(item_id):
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
"method": {"type": "string", "required": True, "nullable": False}, "method": {"type": "string", "required": True, "nullable": False},
"wordlist": {"type": "string", "empty": True, "nullable": True, "required": False}, "wordlist": {
"type": "string",
"empty": True,
"nullable": True,
"required": False,
},
} }
) )
async def api_set_method(): async def api_set_method():

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_paywall") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -7,7 +7,13 @@ from .models import Paywall
async 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:
paywall_id = urlsafe_short_hash() paywall_id = urlsafe_short_hash()
await db.execute( await db.execute(
@ -34,7 +40,9 @@ async def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]

View File

@ -46,7 +46,9 @@ async def m002_redux(db):
) )
await 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 await db.fetchall("SELECT * FROM paywalls_old")]: for row in [
list(row) for row in await db.fetchall("SELECT * FROM paywalls_old")
]:
await db.execute( await db.execute(
""" """
INSERT INTO paywalls ( INSERT INTO paywalls (

View File

@ -16,5 +16,7 @@ async def index():
@paywall_ext.route("/<paywall_id>") @paywall_ext.route("/<paywall_id>")
async def display(paywall_id): async def display(paywall_id):
paywall = await get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.") paywall = await get_paywall(paywall_id) or abort(
HTTPStatus.NOT_FOUND, "Paywall does not exist."
)
return await render_template("paywall/display.html", paywall=paywall) return await render_template("paywall/display.html", paywall=paywall)

View File

@ -17,7 +17,10 @@ async def api_paywalls():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([paywall._asdict() for paywall in await 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"])
@ -26,7 +29,12 @@ async def api_paywalls():
schema={ schema={
"url": {"type": "string", "empty": False, "required": True}, "url": {"type": "string", "empty": False, "required": True},
"memo": {"type": "string", "empty": False, "required": True}, "memo": {"type": "string", "empty": False, "required": True},
"description": {"type": "string", "empty": True, "nullable": True, "required": False}, "description": {
"type": "string",
"empty": True,
"nullable": True,
"required": False,
},
"amount": {"type": "integer", "min": 0, "required": True}, "amount": {"type": "integer", "min": 0, "required": True},
"remembers": {"type": "boolean", "required": True}, "remembers": {"type": "boolean", "required": True},
} }
@ -53,26 +61,41 @@ 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 = await 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 = await 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:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.CREATED,
)
@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 = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
@ -90,6 +113,9 @@ async def api_paywal_check_invoice(paywall_id):
payment = await wallet.get_payment(g.data["payment_hash"]) payment = await wallet.get_payment(g.data["payment_hash"])
await 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,
)
return jsonify({"paid": False}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_subdomains") db = Database("ext_subdomains")
subdomains_ext: Blueprint = Blueprint("subdomains", __name__, static_folder="static", template_folder="templates") subdomains_ext: Blueprint = Blueprint(
"subdomains", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -2,11 +2,20 @@ from lnbits.extensions.subdomains.models import Domains
import httpx, json import httpx, json
async def cloudflare_create_subdomain(domain: Domains, subdomain: str, record_type: str, ip: str): async def cloudflare_create_subdomain(
domain: Domains, subdomain: str, record_type: str, ip: str
):
# Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment # Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment
### SEND REQUEST TO CLOUDFLARE ### SEND REQUEST TO CLOUDFLARE
url = "https://api.cloudflare.com/client/v4/zones/" + domain.cf_zone_id + "/dns_records" url = (
header = {"Authorization": "Bearer " + domain.cf_token, "Content-Type": "application/json"} "https://api.cloudflare.com/client/v4/zones/"
+ domain.cf_zone_id
+ "/dns_records"
)
header = {
"Authorization": "Bearer " + domain.cf_token,
"Content-Type": "application/json",
}
aRecord = subdomain + "." + domain.domain aRecord = subdomain + "." + domain.domain
cf_response = "" cf_response = ""
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -30,8 +39,15 @@ async def cloudflare_create_subdomain(domain: Domains, subdomain: str, record_ty
async def cloudflare_deletesubdomain(domain: Domains, domain_id: str): async def cloudflare_deletesubdomain(domain: Domains, domain_id: str):
url = "https://api.cloudflare.com/client/v4/zones/" + domain.cf_zone_id + "/dns_records" url = (
header = {"Authorization": "Bearer " + domain.cf_token, "Content-Type": "application/json"} "https://api.cloudflare.com/client/v4/zones/"
+ domain.cf_zone_id
+ "/dns_records"
)
header = {
"Authorization": "Bearer " + domain.cf_token,
"Content-Type": "application/json",
}
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
r = await client.delete( r = await client.delete(

View File

@ -23,7 +23,18 @@ async def create_subdomain(
INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type) INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(payment_hash, domain, email, subdomain, ip, wallet, sats, duration, False, record_type), (
payment_hash,
domain,
email,
subdomain,
ip,
wallet,
sats,
duration,
False,
record_type,
),
) )
subdomain = await get_subdomain(payment_hash) subdomain = await get_subdomain(payment_hash)
@ -118,7 +129,18 @@ async def create_domain(
INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types) INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
(domain_id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, 0, allowed_record_types), (
domain_id,
wallet,
domain,
webhook,
cf_token,
cf_zone_id,
description,
cost,
0,
allowed_record_types,
),
) )
domain = await get_domain(domain_id) domain = await get_domain(domain_id)
@ -128,7 +150,9 @@ async def create_domain(
async def update_domain(domain_id: str, **kwargs) -> Domains: async def update_domain(domain_id: str, **kwargs) -> Domains:
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 domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)) await db.execute(
f"UPDATE domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)
)
row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,)) row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,))
assert row, "Newly updated domain couldn't be retrieved" assert row, "Newly updated domain couldn't be retrieved"
return Domains(**row) return Domains(**row)
@ -144,7 +168,9 @@ async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(f"SELECT * FROM domain WHERE wallet IN ({q})", (*wallet_ids,)) rows = await db.fetchall(
f"SELECT * FROM domain WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Domains(**row) for row in rows] return [Domains(**row) for row in rows]

View File

@ -33,7 +33,10 @@ async def on_invoice_paid(payment: Payment) -> None:
### Create subdomain ### Create subdomain
cf_response = cloudflare_create_subdomain( cf_response = cloudflare_create_subdomain(
domain=domain, subdomain=subdomain.subdomain, record_type=subdomain.record_type, ip=subdomain.ip domain=domain,
subdomain=subdomain.subdomain,
record_type=subdomain.record_type,
ip=subdomain.ip,
) )
### Use webhook to notify about cloudflare registration ### Use webhook to notify about cloudflare registration

View File

@ -19,7 +19,9 @@ async def display(domain_id):
domain = await get_domain(domain_id) domain = await get_domain(domain_id)
if not domain: if not domain:
abort(HTTPStatus.NOT_FOUND, "Domain does not exist.") abort(HTTPStatus.NOT_FOUND, "Domain does not exist.")
allowed_records = domain.allowed_record_types.replace('"', "").replace(" ", "").split(",") allowed_records = (
domain.allowed_record_types.replace('"', "").replace(" ", "").split(",")
)
print(allowed_records) print(allowed_records)
return await render_template( return await render_template(
"subdomains/display.html", "subdomains/display.html",

View File

@ -37,7 +37,10 @@ async def api_domains():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK return (
jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]),
HTTPStatus.OK,
)
@subdomains_ext.route("/api/v1/domains", methods=["POST"]) @subdomains_ext.route("/api/v1/domains", methods=["POST"])
@ -98,7 +101,10 @@ async def api_subdomains():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK return (
jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]),
HTTPStatus.OK,
)
@subdomains_ext.route("/api/v1/subdomains/<domain_id>", methods=["POST"]) @subdomains_ext.route("/api/v1/subdomains/<domain_id>", methods=["POST"])
@ -122,24 +128,42 @@ async def api_subdomain_make_subdomain(domain_id):
## If record_type is not one of the allowed ones reject the request ## If record_type is not one of the allowed ones reject the request
if g.data["record_type"] not in domain.allowed_record_types: if g.data["record_type"] not in domain.allowed_record_types:
return jsonify({"message": g.data["record_type"] + "Not a valid record"}), HTTPStatus.BAD_REQUEST return (
jsonify({"message": g.data["record_type"] + "Not a valid record"}),
HTTPStatus.BAD_REQUEST,
)
## If domain already exist in our database reject it ## If domain already exist in our database reject it
if await get_subdomainBySubdomain(g.data["subdomain"]) is not None: if await get_subdomainBySubdomain(g.data["subdomain"]) is not None:
return ( return (
jsonify({"message": g.data["subdomain"] + "." + domain.domain + " domain already taken"}), jsonify(
{
"message": g.data["subdomain"]
+ "."
+ domain.domain
+ " domain already taken"
}
),
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
## Dry run cloudflare... (create and if create is sucessful delete it) ## Dry run cloudflare... (create and if create is sucessful delete it)
cf_response = await cloudflare_create_subdomain( cf_response = await cloudflare_create_subdomain(
domain=domain, subdomain=g.data["subdomain"], record_type=g.data["record_type"], ip=g.data["ip"] domain=domain,
subdomain=g.data["subdomain"],
record_type=g.data["record_type"],
ip=g.data["ip"],
) )
if cf_response["success"] == True: if cf_response["success"] == True:
cloudflare_deletesubdomain(domain=domain, domain_id=cf_response["result"]["id"]) cloudflare_deletesubdomain(domain=domain, domain_id=cf_response["result"]["id"])
else: else:
return ( return (
jsonify({"message": "Problem with cloudflare: " + cf_response["errors"][0]["message"]}), jsonify(
{
"message": "Problem with cloudflare: "
+ cf_response["errors"][0]["message"]
}
),
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
@ -152,12 +176,20 @@ async def api_subdomain_make_subdomain(domain_id):
extra={"tag": "lnsubdomain"}, extra={"tag": "lnsubdomain"},
) )
subdomain = await create_subdomain(payment_hash=payment_hash, wallet=domain.wallet, **g.data) subdomain = await create_subdomain(
payment_hash=payment_hash, wallet=domain.wallet, **g.data
)
if not subdomain: if not subdomain:
return jsonify({"message": "LNsubdomain could not be fetched."}), HTTPStatus.NOT_FOUND return (
jsonify({"message": "LNsubdomain could not be fetched."}),
HTTPStatus.NOT_FOUND,
)
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.OK,
)
@subdomains_ext.route("/api/v1/subdomains/<payment_hash>", methods=["GET"]) @subdomains_ext.route("/api/v1/subdomains/<payment_hash>", methods=["GET"])

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_tpos") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -31,7 +31,9 @@ async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]

View File

@ -16,7 +16,10 @@ async def api_tposs():
if "all_wallets" in request.args: if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return jsonify([tpos._asdict() for tpos in await 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"])
@ -49,7 +52,9 @@ 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 = await get_tpos(tpos_id) tpos = await get_tpos(tpos_id)
@ -58,12 +63,18 @@ async def api_tpos_create_invoice(tpos_id):
try: try:
payment_hash, payment_request = await 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:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.CREATED return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.CREATED,
)
@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"])

View File

@ -3,7 +3,9 @@ from lnbits.db import Database
db = Database("ext_usermanager") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -16,7 +16,9 @@ from .models import Users, Wallets
### Users ### Users
async def create_usermanager_user(user_name: str, wallet_name: str, admin_id: str) -> Users: async def create_usermanager_user(
user_name: str, wallet_name: str, admin_id: str
) -> Users:
account = await create_account() account = await create_account()
user = await get_user(account.id) user = await get_user(account.id)
assert user, "Newly created user couldn't be retrieved" assert user, "Newly created user couldn't be retrieved"
@ -66,7 +68,9 @@ async def delete_usermanager_user(user_id: str) -> None:
### Wallets ### Wallets
async 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 = await create_wallet(user_id=user_id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
await db.execute( await db.execute(
""" """
@ -91,7 +95,9 @@ async def get_usermanager_wallets(user_id: str) -> List[Wallets]:
async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]: async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]:
return await get_payments(wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True) return await get_payments(
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
)
async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None: async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None:

View File

@ -26,7 +26,10 @@ 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 await 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"])
@ -39,7 +42,9 @@ async def api_usermanager_users():
} }
) )
async def api_usermanager_users_create(): async def api_usermanager_users_create():
user = await 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
@ -69,7 +74,9 @@ async def api_usermanager_activate_extension():
user = await 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"]
)
return jsonify({"extension": "updated"}), HTTPStatus.CREATED return jsonify({"extension": "updated"}), HTTPStatus.CREATED
@ -80,7 +87,12 @@ 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 await 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"])
@ -93,7 +105,9 @@ async def api_usermanager_wallets():
} }
) )
async def api_usermanager_wallets_create(): async def api_usermanager_wallets_create():
user = await 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

View File

@ -4,7 +4,9 @@ from lnbits.db import Database
db = Database("ext_withdraw") 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"
)
from .views_api import * # noqa from .views_api import * # noqa

View File

@ -66,7 +66,9 @@ async def get_withdraw_link(link_id: str, num=0) -> Optional[WithdrawLink]:
async 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]:
row = await db.fetchone("SELECT * FROM withdraw_link WHERE unique_hash = ?", (unique_hash,)) row = await 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)
@ -79,14 +81,18 @@ async def get_withdraw_links(wallet_ids: Union[str, List[str]]) -> List[Withdraw
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
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 = await 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]
async 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()])
await db.execute(f"UPDATE withdraw_link SET {q} WHERE id = ?", (*kwargs.values(), link_id)) await 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 = await 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
@ -123,7 +129,9 @@ async def create_hash_check(
async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]:
rowid = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) rowid = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,))
rowlnurl = await db.fetchone("SELECT * FROM hash_check WHERE lnurl_id = ?", (lnurl_id,)) rowlnurl = await db.fetchone(
"SELECT * FROM hash_check WHERE lnurl_id = ?", (lnurl_id,)
)
if not rowlnurl: if not rowlnurl:
await create_hash_check(the_hash, lnurl_id) await create_hash_check(the_hash, lnurl_id)
return {"lnurl": True, "hash": False} return {"lnurl": True, "hash": False}

View File

@ -17,10 +17,16 @@ async def api_lnurl_response(unique_hash):
link = await 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,
)
if link.is_spent: if link.is_spent:
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), HTTPStatus.OK return (
jsonify({"status": "ERROR", "reason": "Withdraw is spent."}),
HTTPStatus.OK,
)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@ -33,10 +39,16 @@ async def api_lnurl_multi_response(unique_hash, id_unique_hash):
link = await 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,
)
if link.is_spent: if link.is_spent:
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), HTTPStatus.OK return (
jsonify({"status": "ERROR", "reason": "Withdraw is spent."}),
HTTPStatus.OK,
)
useslist = link.usescsv.split(",") useslist = link.usescsv.split(",")
found = False found = False
@ -45,7 +57,10 @@ async def api_lnurl_multi_response(unique_hash, id_unique_hash):
if id_unique_hash == shortuuid.uuid(name=tohash): if id_unique_hash == shortuuid.uuid(name=tohash):
found = True found = True
if not found: if not found:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return (
jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}),
HTTPStatus.OK,
)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@ -61,16 +76,27 @@ async def api_lnurl_callback(unique_hash):
now = int(datetime.now().timestamp()) now = int(datetime.now().timestamp())
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,
)
if link.is_spent: if link.is_spent:
return jsonify({"status": "ERROR", "reason": "Withdraw is spent."}), HTTPStatus.OK return (
jsonify({"status": "ERROR", "reason": "Withdraw is spent."}),
HTTPStatus.OK,
)
if link.k1 != k1: if link.k1 != k1:
return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK
if now < link.open_time: if now < link.open_time:
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:
await pay_invoice( await pay_invoice(
@ -85,12 +111,19 @@ async def api_lnurl_callback(unique_hash):
usecv = link.usescsv.split(",") usecv = link.usescsv.split(",")
usescsv += "," + str(usecv[x]) usescsv += "," + str(usecv[x])
usescsv = usescsv[1:] usescsv = usescsv[1:]
changes = {"open_time": link.wait_time + now, "used": link.used + 1, "usescsv": usescsv} changes = {
"open_time": link.wait_time + now,
"used": link.used + 1,
"usescsv": usescsv,
}
await 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,
)
return jsonify({"status": "OK"}), HTTPStatus.OK return jsonify({"status": "OK"}), HTTPStatus.OK

View File

@ -47,7 +47,9 @@ async def m002_change_withdraw_table(db):
""" """
) )
await 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)")
await 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 await db.fetchall("SELECT * FROM withdraw_links")]: for row in [list(row) for row in await db.fetchall("SELECT * FROM withdraw_links")]:
usescsv = "" usescsv = ""

View File

@ -45,13 +45,19 @@ class WithdrawLink(NamedTuple):
_external=True, _external=True,
) )
else: else:
url = url_for("withdraw.api_lnurl_response", unique_hash=self.unique_hash, _external=True) url = url_for(
"withdraw.api_lnurl_response",
unique_hash=self.unique_hash,
_external=True,
)
return lnurl_encode(url) return lnurl_encode(url)
@property @property
def lnurl_response(self) -> LnurlWithdrawResponse: def lnurl_response(self) -> LnurlWithdrawResponse:
url = url_for("withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True) url = url_for(
"withdraw.api_lnurl_callback", unique_hash=self.unique_hash, _external=True
)
return LnurlWithdrawResponse( return LnurlWithdrawResponse(
callback=url, callback=url,
k1=self.k1, k1=self.k1,

View File

@ -17,13 +17,17 @@ async def index():
@withdraw_ext.route("/<link_id>") @withdraw_ext.route("/<link_id>")
async def display(link_id): async def display(link_id):
link = await 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("/img/<link_id>") @withdraw_ext.route("/img/<link_id>")
async def img(link_id): async def img(link_id):
link = await 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."
)
qr = pyqrcode.create(link.lnurl) qr = pyqrcode.create(link.lnurl)
stream = BytesIO() stream = BytesIO()
qr.svg(stream, scale=3) qr.svg(stream, scale=3)
@ -41,13 +45,17 @@ async def img(link_id):
@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 = await 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 = await get_withdraw_link(link_id, count) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") linkk = await get_withdraw_link(link_id, count) or abort(
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
)
links.append(str(linkk.lnurl)) links.append(str(linkk.lnurl))
count = count + 1 count = count + 1
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))

View File

@ -39,7 +39,11 @@ async def api_links():
) )
except LnurlInvalidUrl: except LnurlInvalidUrl:
return ( return (
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}), jsonify(
{
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
}
),
HTTPStatus.UPGRADE_REQUIRED, HTTPStatus.UPGRADE_REQUIRED,
) )
@ -50,7 +54,10 @@ async def api_link_retrieve(link_id):
link = await 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
@ -74,7 +81,11 @@ async def api_link_retrieve(link_id):
async def api_link_create_or_update(link_id=None): async def api_link_create_or_update(link_id=None):
if g.data["max_withdrawable"] < g.data["min_withdrawable"]: if g.data["max_withdrawable"] < g.data["min_withdrawable"]:
return ( return (
jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}), jsonify(
{
"message": "`max_withdrawable` needs to be at least `min_withdrawable`."
}
),
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
@ -89,14 +100,22 @@ async def api_link_create_or_update(link_id=None):
if link_id: if link_id:
link = await 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 = await 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 = await 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"])
@ -105,7 +124,10 @@ async def api_link_delete(link_id):
link = await 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,
)
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

View File

@ -20,15 +20,21 @@ class Extension(NamedTuple):
class ExtensionManager: class ExtensionManager:
def __init__(self): def __init__(self):
self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS self._disabled: List[str] = LNBITS_DISABLED_EXTENSIONS
self._extension_folders: List[str] = [x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions"))][0] self._extension_folders: List[str] = [
x[1] for x in os.walk(os.path.join(LNBITS_PATH, "extensions"))
][0]
@property @property
def extensions(self) -> List[Extension]: def extensions(self) -> List[Extension]:
output = [] output = []
for extension in [ext for ext in self._extension_folders if ext not in self._disabled]: for extension in [
ext for ext in self._extension_folders if ext not in self._disabled
]:
try: try:
with open(os.path.join(LNBITS_PATH, "extensions", extension, "config.json")) as json_file: with open(
os.path.join(LNBITS_PATH, "extensions", extension, "config.json")
) as json_file:
config = json.load(json_file) config = json.load(json_file)
is_valid = True is_valid = True
except Exception: except Exception:
@ -50,7 +56,9 @@ class ExtensionManager:
def get_valid_extensions() -> List[Extension]: def get_valid_extensions() -> List[Extension]:
return [extension for extension in ExtensionManager().extensions if extension.is_valid] return [
extension for extension in ExtensionManager().extensions if extension.is_valid
]
def urlsafe_short_hash() -> str: def urlsafe_short_hash() -> str:
@ -91,7 +99,9 @@ def get_css_vendored(prefer_minified: bool = False) -> List[str]:
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]: def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = [] paths: List[str] = []
for path in glob.glob(os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True): for path in glob.glob(
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext): if path.endswith(".min" + ext):
# path is minified # path is minified
unminified = path.replace(".min" + ext, ext) unminified = path.replace(".min" + ext, ext)

View File

@ -10,7 +10,9 @@ env = Env()
env.read_env() env.read_env()
wallets_module = importlib.import_module("lnbits.wallets") wallets_module = importlib.import_module("lnbits.wallets")
wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet")) wallet_class = getattr(
wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet")
)
ENV = env.str("QUART_ENV", default="production") ENV = env.str("QUART_ENV", default="production")
DEBUG = env.bool("QUART_DEBUG", default=False) or ENV == "development" DEBUG = env.bool("QUART_DEBUG", default=False) or ENV == "development"
@ -18,9 +20,15 @@ HOST = env.str("HOST", default="127.0.0.1")
PORT = env.int("PORT", default=5000) PORT = env.int("PORT", default=5000)
LNBITS_PATH = path.dirname(path.realpath(__file__)) LNBITS_PATH = path.dirname(path.realpath(__file__))
LNBITS_DATA_FOLDER = env.str("LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")) LNBITS_DATA_FOLDER = env.str(
LNBITS_ALLOWED_USERS: List[str] = env.list("LNBITS_ALLOWED_USERS", default=[], subcast=str) "LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list("LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str) )
LNBITS_ALLOWED_USERS: List[str] = env.list(
"LNBITS_ALLOWED_USERS", default=[], subcast=str
)
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
)
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits") LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
WALLET = wallet_class() WALLET = wallet_class()

View File

@ -4,7 +4,11 @@ from typing import Optional, List, Callable
from quart_trio import QuartTrio from quart_trio import QuartTrio
from lnbits.settings import WALLET from lnbits.settings import WALLET
from lnbits.core.crud import get_payments, get_standalone_payment, delete_expired_invoices from lnbits.core.crud import (
get_payments,
get_standalone_payment,
delete_expired_invoices,
)
main_app: Optional[QuartTrio] = None main_app: Optional[QuartTrio] = None
@ -67,7 +71,9 @@ async def invoice_listener(nursery):
async def check_pending_payments(): async def check_pending_payments():
await delete_expired_invoices() await delete_expired_invoices()
while True: while True:
for payment in await get_payments(complete=False, pending=True, exclude_uncheckable=True): for payment in await get_payments(
complete=False, pending=True, exclude_uncheckable=True
):
print(" - checking pending", payment.checking_id) print(" - checking pending", payment.checking_id)
await payment.check_pending() await payment.check_pending()

View File

@ -212,7 +212,12 @@ exchange_rate_providers = {
async def btc_price(currency: str) -> float: async def btc_price(currency: str) -> float:
replacements = {"FROM": "BTC", "from": "btc", "TO": currency.upper(), "to": currency.lower()} replacements = {
"FROM": "BTC",
"from": "btc",
"TO": currency.upper(),
"to": currency.lower(),
}
rates = [] rates = []
send_channel, receive_channel = trio.open_memory_channel(0) send_channel, receive_channel = trio.open_memory_channel(0)

View File

@ -37,7 +37,10 @@ class Wallet(ABC):
@abstractmethod @abstractmethod
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
pass pass

View File

@ -10,13 +10,22 @@ import json
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
Unsupported,
)
class CLightningWallet(Wallet): class CLightningWallet(Wallet):
def __init__(self): def __init__(self):
if LightningRpc is None: # pragma: nocover if LightningRpc is None: # pragma: nocover
raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.") raise ImportError(
"The `pylightning` library must be installed to use `CLightningWallet`."
)
self.rpc = getenv("CLIGHTNING_RPC") self.rpc = getenv("CLIGHTNING_RPC")
self.ln = LightningRpc(self.rpc) self.ln = LightningRpc(self.rpc)
@ -52,7 +61,10 @@ class CLightningWallet(Wallet):
return StatusResponse(error_message, 0) return StatusResponse(error_message, 0)
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
label = "lbl{}".format(random.random()) label = "lbl{}".format(random.random())
msat = amount * 1000 msat = amount * 1000

View File

@ -3,7 +3,13 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class LNbitsWallet(Wallet): class LNbitsWallet(Wallet):
@ -12,7 +18,11 @@ class LNbitsWallet(Wallet):
def __init__(self): def __init__(self):
self.endpoint = getenv("LNBITS_ENDPOINT") self.endpoint = getenv("LNBITS_ENDPOINT")
key = getenv("LNBITS_KEY") or getenv("LNBITS_ADMIN_KEY") or getenv("LNBITS_INVOICE_KEY") key = (
getenv("LNBITS_KEY")
or getenv("LNBITS_ADMIN_KEY")
or getenv("LNBITS_INVOICE_KEY")
)
self.key = {"X-Api-Key": key} self.key = {"X-Api-Key": key}
def status(self) -> StatusResponse: def status(self) -> StatusResponse:
@ -20,7 +30,9 @@ class LNbitsWallet(Wallet):
try: try:
data = r.json() data = r.json()
except: except:
return StatusResponse(f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0) return StatusResponse(
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
)
if r.is_error: if r.is_error:
return StatusResponse(data["message"], 0) return StatusResponse(data["message"], 0)
@ -28,7 +40,10 @@ class LNbitsWallet(Wallet):
return StatusResponse(None, data["balance"]) return StatusResponse(None, data["balance"])
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: Dict = {"out": False, "amount": amount} data: Dict = {"out": False, "amount": amount}
if description_hash: if description_hash:
@ -41,7 +56,12 @@ class LNbitsWallet(Wallet):
headers=self.key, headers=self.key,
json=data, json=data,
) )
ok, checking_id, payment_request, error_message = not r.is_error, None, None, None ok, checking_id, payment_request, error_message = (
not r.is_error,
None,
None,
None,
)
if r.is_error: if r.is_error:
error_message = r.json()["message"] error_message = r.json()["message"]
@ -52,7 +72,11 @@ class LNbitsWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = httpx.post(url=f"{self.endpoint}/api/v1/payments", headers=self.key, json={"out": True, "bolt11": bolt11}) r = httpx.post(
url=f"{self.endpoint}/api/v1/payments",
headers=self.key,
json={"out": True, "bolt11": bolt11},
)
ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None ok, checking_id, fee_msat, error_message = not r.is_error, None, 0, None
if r.is_error: if r.is_error:
@ -64,7 +88,9 @@ class LNbitsWallet(Wallet):
return PaymentResponse(ok, checking_id, fee_msat, error_message) return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, checking_id: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = httpx.get(url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key) r = httpx.get(
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
)
if r.is_error: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
@ -72,7 +98,9 @@ class LNbitsWallet(Wallet):
return PaymentStatus(r.json()["paid"]) return PaymentStatus(r.json()["paid"])
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = httpx.get(url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key) r = httpx.get(
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
)
if r.is_error: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)

View File

@ -15,7 +15,13 @@ import hashlib
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
def get_ssl_context(cert_path: str): def get_ssl_context(cert_path: str):
@ -76,10 +82,14 @@ def stringify_checking_id(r_hash: bytes) -> str:
class LndWallet(Wallet): class LndWallet(Wallet):
def __init__(self): def __init__(self):
if lndgrpc is None: # pragma: nocover if lndgrpc is None: # pragma: nocover
raise ImportError("The `lndgrpc` library must be installed to use `LndWallet`.") raise ImportError(
"The `lndgrpc` library must be installed to use `LndWallet`."
)
if purerpc is None: # pragma: nocover if purerpc is None: # pragma: nocover
raise ImportError("The `purerpc` library must be installed to use `LndWallet`.") raise ImportError(
"The `purerpc` library must be installed to use `LndWallet`."
)
endpoint = getenv("LND_GRPC_ENDPOINT") endpoint = getenv("LND_GRPC_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
@ -111,7 +121,10 @@ class LndWallet(Wallet):
return StatusResponse(None, resp.balance * 1000) return StatusResponse(None, resp.balance * 1000)
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
params: Dict = {"value": amount, "expiry": 600, "private": True} params: Dict = {"value": amount, "expiry": 600, "private": True}

View File

@ -5,7 +5,13 @@ import base64
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class LndRestWallet(Wallet): class LndRestWallet(Wallet):
@ -14,7 +20,9 @@ class LndRestWallet(Wallet):
def __init__(self): def __init__(self):
endpoint = getenv("LND_REST_ENDPOINT") endpoint = getenv("LND_REST_ENDPOINT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
endpoint = "https://" + endpoint if not endpoint.startswith("http") else endpoint endpoint = (
"https://" + endpoint if not endpoint.startswith("http") else endpoint
)
self.endpoint = endpoint self.endpoint = endpoint
macaroon = ( macaroon = (
@ -47,14 +55,19 @@ class LndRestWallet(Wallet):
return StatusResponse(None, int(data["balance"]) * 1000) return StatusResponse(None, int(data["balance"]) * 1000)
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: Dict = { data: Dict = {
"value": amount, "value": amount,
"private": True, "private": True,
} }
if description_hash: if description_hash:
data["description_hash"] = base64.b64encode(description_hash).decode("ascii") data["description_hash"] = base64.b64encode(description_hash).decode(
"ascii"
)
else: else:
data["memo"] = memo or "" data["memo"] = memo or ""
@ -131,7 +144,12 @@ class LndRestWallet(Wallet):
# check payment.status: # check payment.status:
# https://api.lightning.community/rest/index.html?python#peersynctype # https://api.lightning.community/rest/index.html?python#peersynctype
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False} statuses = {
"UNKNOWN": None,
"IN_FLIGHT": None,
"SUCCEEDED": True,
"FAILED": False,
}
# for some reason our checking_ids are in base64 but the payment hashes # for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird # returned here are in hex, lnd is weird

View File

@ -6,7 +6,13 @@ from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from quart import request from quart import request
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class LNPayWallet(Wallet): class LNPayWallet(Wallet):
@ -31,7 +37,8 @@ class LNPayWallet(Wallet):
data = r.json() data = r.json()
if data["statusType"]["name"] != "active": if data["statusType"]["name"] != "active":
return StatusResponse( return StatusResponse(
f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}", 0 f"Wallet {data['user_label']} (data['id']) not active, but {data['statusType']['name']}",
0,
) )
return StatusResponse(None, data["balance"] * 1000) return StatusResponse(None, data["balance"] * 1000)
@ -78,7 +85,9 @@ class LNPayWallet(Wallet):
try: try:
data = r.json() data = r.json()
except: except:
return PaymentResponse(False, None, 0, None, f"Got invalid JSON: {r.text[:200]}") return PaymentResponse(
False, None, 0, None, f"Got invalid JSON: {r.text[:200]}"
)
if r.is_error: if r.is_error:
return PaymentResponse(False, None, 0, None, data["message"]) return PaymentResponse(False, None, 0, None, data["message"])
@ -115,7 +124,11 @@ class LNPayWallet(Wallet):
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
print(f"got something wrong on lnpay webhook endpoint: {text[:200]}") print(f"got something wrong on lnpay webhook endpoint: {text[:200]}")
data = None data = None
if type(data) is not dict or "event" not in data or data["event"].get("name") != "wallet_receive": if (
type(data) is not dict
or "event" not in data
or data["event"].get("name") != "wallet_receive"
):
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
lntx_id = data["data"]["wtx"]["lnTx"]["id"] lntx_id = data["data"]["wtx"]["lnTx"]["id"]

View File

@ -4,7 +4,13 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class LntxbotWallet(Wallet): class LntxbotWallet(Wallet):
@ -14,7 +20,11 @@ class LntxbotWallet(Wallet):
endpoint = getenv("LNTXBOT_API_ENDPOINT") endpoint = getenv("LNTXBOT_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
key = getenv("LNTXBOT_KEY") or getenv("LNTXBOT_ADMIN_KEY") or getenv("LNTXBOT_INVOICE_KEY") key = (
getenv("LNTXBOT_KEY")
or getenv("LNTXBOT_ADMIN_KEY")
or getenv("LNTXBOT_INVOICE_KEY")
)
self.auth = {"Authorization": f"Basic {key}"} self.auth = {"Authorization": f"Basic {key}"}
def status(self) -> StatusResponse: def status(self) -> StatusResponse:
@ -26,7 +36,9 @@ class LntxbotWallet(Wallet):
try: try:
data = r.json() data = r.json()
except: except:
return StatusResponse(f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0) return StatusResponse(
f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0
)
if data.get("error"): if data.get("error"):
return StatusResponse(data["message"], 0) return StatusResponse(data["message"], 0)
@ -34,7 +46,10 @@ class LntxbotWallet(Wallet):
return StatusResponse(None, data["BTC"]["AvailableBalance"] * 1000) return StatusResponse(None, data["BTC"]["AvailableBalance"] * 1000)
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
data: Dict = {"amt": str(amount)} data: Dict = {"amt": str(amount)}
if description_hash: if description_hash:

View File

@ -7,7 +7,14 @@ from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from quart import request, url_for from quart import request, url_for
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
Unsupported,
)
class OpenNodeWallet(Wallet): class OpenNodeWallet(Wallet):
@ -17,7 +24,11 @@ class OpenNodeWallet(Wallet):
endpoint = getenv("OPENNODE_API_ENDPOINT") endpoint = getenv("OPENNODE_API_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
key = getenv("OPENNODE_KEY") or getenv("OPENNODE_ADMIN_KEY") or getenv("OPENNODE_INVOICE_KEY") key = (
getenv("OPENNODE_KEY")
or getenv("OPENNODE_ADMIN_KEY")
or getenv("OPENNODE_INVOICE_KEY")
)
self.auth = {"Authorization": key} self.auth = {"Authorization": key}
def status(self) -> StatusResponse: def status(self) -> StatusResponse:
@ -37,7 +48,10 @@ class OpenNodeWallet(Wallet):
return StatusResponse(None, data["balance"]["BTC"] / 100_000_000_000) return StatusResponse(None, data["balance"]["BTC"] / 100_000_000_000)
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
if description_hash: if description_hash:
raise Unsupported("description_hash") raise Unsupported("description_hash")
@ -93,7 +107,13 @@ class OpenNodeWallet(Wallet):
if r.is_error: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False} statuses = {
"initial": None,
"pending": None,
"confirmed": True,
"error": False,
"failed": False,
}
return PaymentStatus(statuses[r.json()["data"]["status"]]) return PaymentStatus(statuses[r.json()["data"]["status"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:

View File

@ -5,7 +5,13 @@ import httpx
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
)
class SparkError(Exception): class SparkError(Exception):
@ -24,7 +30,9 @@ class SparkWallet(Wallet):
def __getattr__(self, key): def __getattr__(self, key):
def call(*args, **kwargs): def call(*args, **kwargs):
if args and kwargs: if args and kwargs:
raise TypeError(f"must supply either named arguments or a list of arguments, not both: {args} {kwargs}") raise TypeError(
f"must supply either named arguments or a list of arguments, not both: {args} {kwargs}"
)
elif args: elif args:
params = args params = args
elif kwargs: elif kwargs:
@ -67,7 +75,10 @@ class SparkWallet(Wallet):
) )
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
label = "lbs{}".format(random.random()) label = "lbs{}".format(random.random())
checking_id = label checking_id = label
@ -81,7 +92,10 @@ class SparkWallet(Wallet):
) )
else: else:
r = self.invoice( r = self.invoice(
msatoshi=amount * 1000, label=label, description=memo or "", exposeprivatechannels=True msatoshi=amount * 1000,
label=label,
description=memo or "",
exposeprivatechannels=True,
) )
ok, payment_request, error_message = True, r["bolt11"], "" ok, payment_request, error_message = True, r["bolt11"], ""
except (SparkError, UnknownError) as e: except (SparkError, UnknownError) as e:

View File

@ -1,11 +1,21 @@
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from .base import StatusResponse, InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import (
StatusResponse,
InvoiceResponse,
PaymentResponse,
PaymentStatus,
Wallet,
Unsupported,
)
class VoidWallet(Wallet): class VoidWallet(Wallet):
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
) -> InvoiceResponse: ) -> InvoiceResponse:
raise Unsupported("") raise Unsupported("")

View File

@ -1,2 +0,0 @@
[tool.black]
line-length = 120