remove exception to black line-length and reformat.
This commit is contained in:
parent
3333f1f3f3
commit
42bd5ea989
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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;")
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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.",
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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("")
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[tool.black]
|
|
||||||
line-length = 120
|
|
Loading…
Reference in New Issue
Block a user