Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c820456500 |
@@ -42,6 +42,8 @@ lnbits/static/bundle.min.css.old
|
||||
lnbits/static/bundle-components.min.js.old
|
||||
lnbits/upgrades
|
||||
docker
|
||||
generated
|
||||
openapi.json*
|
||||
|
||||
# Nix
|
||||
*result*
|
||||
|
||||
+2
-4
@@ -19,8 +19,7 @@ ENV POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=1 \
|
||||
POETRY_CACHE_DIR=/tmp/poetry_cache
|
||||
|
||||
ARG POETRY_INSTALL_ARGS="--only main"
|
||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||
RUN poetry install --only main
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
@@ -47,8 +46,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=builder /app/.venv .venv
|
||||
|
||||
ARG POETRY_INSTALL_ARGS="--only main"
|
||||
RUN poetry install ${POETRY_INSTALL_ARGS}
|
||||
RUN poetry install --only main
|
||||
|
||||
ENV LNBITS_PORT="5000"
|
||||
ENV LNBITS_HOST="0.0.0.0"
|
||||
|
||||
@@ -99,9 +99,13 @@ openapi:
|
||||
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
|
||||
# kill -9 %1
|
||||
|
||||
bak:
|
||||
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
|
||||
#
|
||||
generate:
|
||||
rm -rf generated
|
||||
mkdir generated
|
||||
rm -f openapi.json
|
||||
wget localhost:5000/openapi.json
|
||||
npm run generate
|
||||
rm openapi.json
|
||||
|
||||
sass:
|
||||
npm run sass
|
||||
|
||||
@@ -78,24 +78,11 @@ poetry run lnbits-cli --help
|
||||
|
||||
```sh
|
||||
cd lnbits
|
||||
# Stop LNbits with `ctrl + x` or with service manager
|
||||
# sudo systemctl stop lnbits
|
||||
|
||||
# Update LNbits
|
||||
git pull --rebase
|
||||
|
||||
# Check your poetry version with
|
||||
poetry env list
|
||||
# If version is less 3.12, update it by running
|
||||
poetry env use python3.12
|
||||
poetry env remove python3.9
|
||||
poetry env list
|
||||
|
||||
# Run install and start LNbits with
|
||||
# Stop LNbits with `ctrl + x`
|
||||
git pull
|
||||
# Keep your poetry install up to date, this can be done with `poetry self update`
|
||||
poetry install --only main
|
||||
poetry run lnbits
|
||||
|
||||
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
|
||||
# Start LNbits with `poetry run lnbits`
|
||||
```
|
||||
|
||||
## Option 2: Install script (on Debian/Ubuntu)
|
||||
@@ -150,7 +137,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
||||
|
||||
## Option 4: Docker
|
||||
|
||||
Use latest version from Docker Hub.
|
||||
use latest version from docker hub
|
||||
|
||||
```sh
|
||||
docker pull lnbits/lnbits
|
||||
@@ -159,7 +146,7 @@ mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
```
|
||||
|
||||
Build the image yourself.
|
||||
build the image yourself
|
||||
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
@@ -170,12 +157,6 @@ mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
```
|
||||
|
||||
You can optionally override the arguments that are passed to `poetry install` during the build process by setting the Docker build argument named `POETRY_INSTALL_ARGS`. For example, to enable the Breez funding source, build the Docker image with the command:
|
||||
|
||||
```sh
|
||||
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
||||
```
|
||||
|
||||
## Option 5: Fly.io
|
||||
|
||||
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
|
||||
@@ -583,12 +564,3 @@ docker run --detach --publish 5000:5000 --name lnbits -e "LNBITS_BACKEND_WALLET_
|
||||
```
|
||||
|
||||
Finally you can access your lnbits on your machine at port 5000.
|
||||
|
||||
### FreeBSD notes
|
||||
|
||||
Currently there is an issue with secp256k1 0.14.0 on FreeBSD. Thanks to @GitKalle
|
||||
|
||||
1. Install package `py311-secp256k1` with `pkg install py311-secp256k1`
|
||||
2. Change version in `pyproject.toml` from 0.14.0 to 0.13.2
|
||||
3. Rewrite `poetry.lock` file with command `poetry lock`
|
||||
4. Follow install instruction with Poetry
|
||||
|
||||
@@ -51,7 +51,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
|
||||
|
||||
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
|
||||
|
||||
To encrypt your macaroon, run `poetry run lnbits-cli encrypt macaroon`.
|
||||
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
|
||||
|
||||
### LNbits
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import importlib
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
@@ -41,9 +40,7 @@ from lnbits.core.views.extension_api import (
|
||||
api_uninstall_extension,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.wallets.base import Wallet
|
||||
from lnbits.wallets.macaroon import load_macaroon
|
||||
|
||||
|
||||
def coro(f):
|
||||
@@ -82,20 +79,6 @@ def extensions():
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def encrypt():
|
||||
"""
|
||||
Encryption commands
|
||||
"""
|
||||
|
||||
|
||||
@lnbits_cli.group()
|
||||
def decrypt():
|
||||
"""
|
||||
Decryption commands
|
||||
"""
|
||||
|
||||
|
||||
def get_super_user() -> Optional[str]:
|
||||
"""Get the superuser"""
|
||||
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
|
||||
@@ -496,56 +479,6 @@ async def extensions_uninstall(
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
@encrypt.command("macaroon")
|
||||
def encrypt_macaroon():
|
||||
"""Encrypts a macaroon (LND wallets)"""
|
||||
_macaroon = getpass("Enter macaroon: ")
|
||||
try:
|
||||
macaroon = load_macaroon(_macaroon)
|
||||
except Exception as ex:
|
||||
click.echo(f"Error loading macaroon: {ex}")
|
||||
return
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
encrypted_macaroon = aes.encrypt(bytes.fromhex(macaroon))
|
||||
except Exception as ex:
|
||||
click.echo(f"Error encrypting macaroon: {ex}")
|
||||
return
|
||||
click.echo("Encrypted macaroon: ")
|
||||
click.echo(encrypted_macaroon)
|
||||
|
||||
|
||||
@encrypt.command("aes")
|
||||
@click.option("-p", "--payload", required=True, help="Payload to encrypt.")
|
||||
def encrypt_aes(payload: str):
|
||||
"""AES encrypts a payload"""
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
encrypted = aes.encrypt(payload.encode())
|
||||
except Exception as ex:
|
||||
click.echo(f"Error encrypting payload: {ex}")
|
||||
return
|
||||
click.echo("Encrypted payload: ")
|
||||
click.echo(encrypted)
|
||||
|
||||
|
||||
@decrypt.command("aes")
|
||||
@click.option("-p", "--payload", required=True, help="Payload to decrypt.")
|
||||
def decrypt_aes(payload: str):
|
||||
"""AES decrypts a payload"""
|
||||
key = getpass("Enter encryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
try:
|
||||
decrypted = aes.decrypt(payload)
|
||||
except Exception as ex:
|
||||
click.echo(f"Error decrypting payload: {ex}")
|
||||
return
|
||||
click.echo("Decrypted payload: ")
|
||||
click.echo(decrypted)
|
||||
|
||||
|
||||
def main():
|
||||
"""main function"""
|
||||
lnbits_cli()
|
||||
|
||||
@@ -43,7 +43,6 @@ from .settings import (
|
||||
delete_admin_settings,
|
||||
get_admin_settings,
|
||||
get_super_settings,
|
||||
reset_core_settings,
|
||||
update_admin_settings,
|
||||
update_super_user,
|
||||
)
|
||||
@@ -130,7 +129,6 @@ __all__ = [
|
||||
"get_super_settings",
|
||||
"update_admin_settings",
|
||||
"update_super_user",
|
||||
"reset_core_settings",
|
||||
# tinyurl
|
||||
"create_tinyurl",
|
||||
"delete_tinyurl",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from time import time
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet, get_wallets_ids
|
||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import PaymentState
|
||||
from lnbits.db import Connection, DateTrunc, Filters, Page
|
||||
@@ -95,7 +95,6 @@ async def get_latest_payments_by_extension(
|
||||
async def get_payments_paginated(
|
||||
*,
|
||||
wallet_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
failed: bool = False,
|
||||
@@ -122,13 +121,6 @@ async def get_payments_paginated(
|
||||
if wallet_id:
|
||||
values["wallet_id"] = wallet_id
|
||||
clause.append("wallet_id = :wallet_id")
|
||||
elif user_id:
|
||||
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||
"no-wallets-for-user"
|
||||
]
|
||||
# wallet ids are safe to use in sql queries
|
||||
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
|
||||
clause.append(f""" wallet_id IN ({", ".join(wallet_ids_str)}) """)
|
||||
|
||||
if complete and pending:
|
||||
clause.append(
|
||||
@@ -516,7 +508,7 @@ async def is_internal_status_success(
|
||||
return payment.status == PaymentState.SUCCESS.value
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
||||
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET webhook_status = :status
|
||||
|
||||
@@ -61,23 +61,7 @@ async def update_super_user(super_user: str) -> SuperSettings:
|
||||
|
||||
|
||||
async def delete_admin_settings(tag: Optional[str] = "core") -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM system_settings WHERE tag = :tag",
|
||||
{"tag": tag},
|
||||
)
|
||||
|
||||
|
||||
async def reset_core_settings() -> None:
|
||||
await db.execute(
|
||||
"""
|
||||
DELETE FROM system_settings WHERE tag = 'core'
|
||||
AND id NOT IN (
|
||||
'super_user',
|
||||
'lnbits_webpush_pubkey',
|
||||
'lnbits_webpush_privkey'
|
||||
)
|
||||
""",
|
||||
)
|
||||
await db.execute("DELETE FROM settings WHERE tag = :tag", {"tag": tag})
|
||||
|
||||
|
||||
async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSettings:
|
||||
|
||||
@@ -135,20 +135,6 @@ async def get_wallets(
|
||||
)
|
||||
|
||||
|
||||
async def get_wallets_ids(
|
||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||
) -> list[str]:
|
||||
where = "AND deleted = :deleted" if deleted is not None else ""
|
||||
result: list[dict] = await (conn or db).fetchall(
|
||||
f"""
|
||||
SELECT id FROM wallets
|
||||
WHERE "user" = :user {where}
|
||||
""",
|
||||
{"user": user_id, "deleted": deleted},
|
||||
)
|
||||
return [row["id"] for row in result]
|
||||
|
||||
|
||||
async def get_wallets_count():
|
||||
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
|
||||
row = result.mappings().first()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
import bcrypt
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -11,15 +9,6 @@ def _do_nothing(*_):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SolveBugBcryptWarning:
|
||||
__version__: str = getattr(bcrypt, "__version__") # noqa
|
||||
|
||||
|
||||
# fix annoying warning in the logs
|
||||
setattr(bcrypt, "__about__", SolveBugBcryptWarning()) # noqa
|
||||
|
||||
|
||||
class CoreAppExtra:
|
||||
register_new_ext_routes: Callable = _do_nothing
|
||||
register_new_ratelimiter: Callable
|
||||
|
||||
@@ -64,7 +64,7 @@ class Payment(BaseModel):
|
||||
memo: str | None = None
|
||||
expiry: datetime | None = None
|
||||
webhook: str | None = None
|
||||
webhook_status: str | None = None
|
||||
webhook_status: int | None = None
|
||||
preimage: str | None = None
|
||||
tag: str | None = None
|
||||
extension: str | None = None
|
||||
|
||||
@@ -205,23 +205,24 @@ async def dispatch_webhook(payment: Payment):
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
|
||||
if not payment.webhook:
|
||||
return await mark_webhook_sent(payment.payment_hash, "-1")
|
||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
check_callback_url(payment.webhook)
|
||||
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, str(exc.response.status_code))
|
||||
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
||||
logger.warning(
|
||||
f"webhook returned a bad status_code: {exc.response.status_code} "
|
||||
f"while requesting {exc.request.url!r}."
|
||||
)
|
||||
except httpx.RequestError:
|
||||
await mark_webhook_sent(payment.payment_hash, "-1")
|
||||
await mark_webhook_sent(payment.payment_hash, -1)
|
||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||
|
||||
|
||||
@@ -229,21 +230,18 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
try:
|
||||
await send_ws_payment_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||
logger.error("Error sending websocket payment notification", e)
|
||||
try:
|
||||
send_chat_payment_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending chat payment notification {e!s}")
|
||||
logger.error("Error sending chat payment notification", e)
|
||||
try:
|
||||
await send_payment_push_notification(wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending push payment notification {e!s}")
|
||||
logger.error("Error sending push payment notification", e)
|
||||
|
||||
try:
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
except Exception as e:
|
||||
logger.error(f"Error dispatching webhook: {e!s}")
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
|
||||
|
||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||
|
||||
@@ -44,9 +44,6 @@ from ..models import (
|
||||
)
|
||||
from .notifications import send_payment_notification
|
||||
|
||||
payment_lock = asyncio.Lock()
|
||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
async def pay_invoice(
|
||||
*,
|
||||
@@ -82,12 +79,12 @@ async def pay_invoice(
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||
payment = await _pay_invoice(wallet, create_payment_model, conn)
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
await _credit_service_fee_wallet(payment, new_conn)
|
||||
|
||||
return payment
|
||||
return payment
|
||||
|
||||
|
||||
async def create_invoice(
|
||||
@@ -135,29 +132,29 @@ async def create_invoice(
|
||||
status="failed",
|
||||
)
|
||||
|
||||
payment_response = await funding_source.create_invoice(
|
||||
(
|
||||
ok,
|
||||
checking_id,
|
||||
payment_request,
|
||||
error_message,
|
||||
) = await funding_source.create_invoice(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
description_hash=description_hash,
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=expiry or settings.lightning_invoice_expiry,
|
||||
)
|
||||
if (
|
||||
not payment_response.ok
|
||||
or not payment_response.payment_request
|
||||
or not payment_response.checking_id
|
||||
):
|
||||
if not ok or not payment_request or not checking_id:
|
||||
raise InvoiceError(
|
||||
message=payment_response.error_message or "unexpected backend error.",
|
||||
status="pending",
|
||||
error_message or "unexpected backend error.", status="pending"
|
||||
)
|
||||
invoice = bolt11_decode(payment_response.payment_request)
|
||||
|
||||
invoice = bolt11_decode(payment_request)
|
||||
|
||||
create_payment_model = CreatePayment(
|
||||
wallet_id=wallet_id,
|
||||
bolt11=payment_response.payment_request,
|
||||
bolt11=payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
preimage=payment_response.preimage,
|
||||
amount_msat=amount_sat * 1000,
|
||||
expiry=invoice.expiry_date,
|
||||
memo=memo,
|
||||
@@ -166,7 +163,7 @@ async def create_invoice(
|
||||
)
|
||||
|
||||
payment = await create_payment(
|
||||
checking_id=payment_response.checking_id,
|
||||
checking_id=checking_id,
|
||||
data=create_payment_model,
|
||||
conn=conn,
|
||||
)
|
||||
@@ -202,7 +199,6 @@ def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||
|
||||
|
||||
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
|
||||
amount_msat = abs(amount_msat)
|
||||
return settings.fee_reserve(amount_msat, internal)
|
||||
|
||||
|
||||
@@ -413,16 +409,13 @@ async def get_payments_daily_stats(
|
||||
|
||||
data: list[PaymentDailyStats] = []
|
||||
|
||||
def _tz(dt: datetime) -> datetime:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
start_date = min(_tz(data_in[0].date), _tz(data_out[0].date))
|
||||
end_date = max(_tz(data_in[-1].date), _tz(data_out[-1].date))
|
||||
start_date = min(data_in[0].date, data_out[0].date)
|
||||
end_date = max(data_in[-1].date, data_out[-1].date)
|
||||
delta = timedelta(days=1)
|
||||
while start_date <= end_date:
|
||||
|
||||
data_in_point = next((x for x in data_in if _tz(x.date) == start_date), _none)
|
||||
data_out_point = next((x for x in data_out if _tz(x.date) == start_date), _none)
|
||||
data_in_point = next((x for x in data_in if x.date == start_date), _none)
|
||||
data_out_point = next((x for x in data_out if x.date == start_date), _none)
|
||||
|
||||
balance_total += data_in_point.balance + data_out_point.balance
|
||||
data.append(
|
||||
@@ -444,27 +437,11 @@ async def get_payments_daily_stats(
|
||||
return data
|
||||
|
||||
|
||||
async def _pay_invoice(
|
||||
wallet_id: str,
|
||||
create_payment_model: CreatePayment,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
async with payment_lock:
|
||||
if wallet_id not in wallets_payments_lock:
|
||||
wallets_payments_lock[wallet_id] = asyncio.Lock()
|
||||
|
||||
async with wallets_payments_lock[wallet_id]:
|
||||
# get the wallet again to make sure we have the latest balance
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not wallet:
|
||||
raise PaymentError(
|
||||
f"Could not fetch wallet '{wallet_id}'.", status="failed"
|
||||
)
|
||||
|
||||
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
||||
if not payment:
|
||||
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
|
||||
return payment
|
||||
async def _pay_invoice(wallet, create_payment_model, conn):
|
||||
payment = await _pay_internal_invoice(wallet, create_payment_model, conn)
|
||||
if not payment:
|
||||
payment = await _pay_external_invoice(wallet, create_payment_model, conn)
|
||||
return payment
|
||||
|
||||
|
||||
async def _pay_internal_invoice(
|
||||
@@ -499,15 +476,12 @@ async def _pay_internal_invoice(
|
||||
):
|
||||
raise PaymentError("Invalid invoice. Bolt11 changed.", status="failed")
|
||||
|
||||
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=True)
|
||||
fee_reserve_total_msat = fee_reserve_total(abs(amount_msat), internal=True)
|
||||
create_payment_model.fee = abs(fee_reserve_total_msat)
|
||||
|
||||
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||
raise PaymentError("Insufficient balance.", status="failed")
|
||||
|
||||
# release the preimage
|
||||
create_payment_model.preimage = internal_invoice.preimage
|
||||
|
||||
internal_id = f"internal_{create_payment_model.payment_hash}"
|
||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||
payment = await create_payment(
|
||||
@@ -545,8 +519,6 @@ async def _pay_external_invoice(
|
||||
|
||||
fee_reserve_total_msat = fee_reserve_total(amount_msat, internal=False)
|
||||
|
||||
if wallet.balance_msat < abs(amount_msat):
|
||||
raise PaymentError("Insufficient balance.", status="failed")
|
||||
if wallet.balance_msat < abs(amount_msat) + fee_reserve_total_msat:
|
||||
raise PaymentError(
|
||||
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const ALLOWED_REGISTER = {{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }};
|
||||
</script>
|
||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||
{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
@@ -148,7 +151,7 @@
|
||||
<username-password
|
||||
v-if="authMethod != 'user-id-only'"
|
||||
:allowed_new_users="allowedRegister"
|
||||
:auth-methods="LNBITS_AUTH_METHODS"
|
||||
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
||||
:auth-action="authAction"
|
||||
v-model:user-name="username"
|
||||
v-model:password_1="password"
|
||||
@@ -179,6 +182,7 @@
|
||||
>
|
||||
<span v-text="$t('new_user_not_allowed')"></span>
|
||||
</p>
|
||||
|
||||
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||
<span v-text="$t('existing_account_question')"></span>
|
||||
|
||||
@@ -248,7 +252,7 @@
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
{% if AD_SPACE_ENABLED and AD_SPACE %}
|
||||
{% if AD_SPACE %}
|
||||
<div class="q-pt-md full-width">
|
||||
<div class="row justify-center q-mb-xl">
|
||||
<div class="full-width text-center">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
|
||||
%}
|
||||
<div class="row q-col-gutter-md">
|
||||
{% if HIDE_API and AD_SPACE_ENABLED and AD_SPACE %}
|
||||
{% if HIDE_API and AD_SPACE %}
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
{% elif HIDE_API %}
|
||||
<div class="col-12 q-gutter-y-md">
|
||||
@@ -31,7 +31,7 @@
|
||||
<q-card-section style="height: 130px">
|
||||
<div class="row q-gutter-md">
|
||||
<div
|
||||
v-if="isSatsDenomination"
|
||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats'"
|
||||
class="col-1"
|
||||
style="max-width: 30px"
|
||||
>
|
||||
@@ -468,7 +468,7 @@
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endif %} {% if AD_SPACE_ENABLED and AD_SPACE %}
|
||||
{% endif %} {% if AD_SPACE %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
|
||||
@@ -639,7 +639,7 @@
|
||||
>
|
||||
<span
|
||||
v-if="receive.lnurl"
|
||||
v-text="`${$t('withdraw_from')} ${receive.lnurl.domain}`"
|
||||
v-text="$t('withdraw_from') + receive.lnurl.domain"
|
||||
></span>
|
||||
<span v-else v-text="$t('create_invoice')"></span>
|
||||
</q-btn>
|
||||
@@ -961,8 +961,8 @@
|
||||
dense
|
||||
v-model.number="parse.data.amount"
|
||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||
:mask="parse.data.unit == 'sat' ? '#' : ''"
|
||||
:step="parse.data.unit == 'sat' ? '1': '0.01'"
|
||||
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
|
||||
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
|
||||
@@ -29,7 +29,7 @@ from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||
from lnbits.tasks import invoice_listeners
|
||||
|
||||
from .. import core_app_extra
|
||||
from ..crud import get_admin_settings, reset_core_settings, update_admin_settings
|
||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||
|
||||
admin_router = APIRouter(tags=["Admin UI"], prefix="/admin")
|
||||
file_upload = File(...)
|
||||
@@ -113,7 +113,7 @@ async def api_reset_settings(field_name: str):
|
||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||
await reset_core_settings()
|
||||
await delete_admin_settings()
|
||||
server_restart.set()
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
|
||||
async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Login by 'Username and Password' not allowed."
|
||||
HTTPStatus.UNAUTHORIZED, "Login by 'Username and Password' not allowed."
|
||||
)
|
||||
account = await get_account_by_username_or_email(data.username)
|
||||
if not account or not account.verify_password(data.password):
|
||||
@@ -85,7 +85,9 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
||||
@auth_router.post("/nostr", description="Login via Nostr")
|
||||
async def nostr_login(request: Request) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||
raise HTTPException(
|
||||
HTTPStatus.UNAUTHORIZED, "Login with Nostr Auth not allowed."
|
||||
)
|
||||
event = _nostr_nip98_event(request)
|
||||
account = await get_account_by_pubkey(event["pubkey"])
|
||||
if not account:
|
||||
@@ -102,7 +104,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
||||
async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
"Login by 'User ID' not allowed.",
|
||||
)
|
||||
account = await get_account(data.usr)
|
||||
@@ -110,7 +112,7 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
||||
if account.is_admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Admin users cannot login with user id only."
|
||||
HTTPStatus.UNAUTHORIZED, "Admin users cannot login with user id only."
|
||||
)
|
||||
return _auth_success_response(account.username, account.id, account.email)
|
||||
|
||||
@@ -240,7 +242,7 @@ async def login_with_sso_provider(
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
f"Login by '{provider}' not allowed.",
|
||||
)
|
||||
|
||||
@@ -255,7 +257,7 @@ async def handle_oauth_token(request: Request, provider: str) -> RedirectRespons
|
||||
provider_sso = _new_sso(provider)
|
||||
if not provider_sso:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
f"Login by '{provider}' not allowed.",
|
||||
)
|
||||
|
||||
@@ -283,7 +285,7 @@ async def logout() -> JSONResponse:
|
||||
async def register(data: RegisterUser) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
"Register by 'Username and Password' not allowed.",
|
||||
)
|
||||
|
||||
@@ -373,7 +375,7 @@ async def update_password(
|
||||
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
|
||||
HTTPStatus.UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
|
||||
)
|
||||
|
||||
assert data.password == data.password_repeat, "Passwords do not match."
|
||||
@@ -447,7 +449,7 @@ async def update(
|
||||
@auth_router.put("/first_install")
|
||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||
if not settings.first_install:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "This is not your first install")
|
||||
account = await get_account(settings.super_user)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||
@@ -470,10 +472,10 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
||||
|
||||
if verified_user_id:
|
||||
if account:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Email already used.")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Email already used.")
|
||||
account = await get_account(verified_user_id)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Cannot verify user email.")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Cannot verify user email.")
|
||||
redirect_path = "/account"
|
||||
|
||||
if account:
|
||||
@@ -497,12 +499,9 @@ def _auth_success_response(
|
||||
sub=username or "", usr=user_id, email=email, auth_time=int(time())
|
||||
)
|
||||
access_token = create_access_token(data=payload.dict())
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||
response.set_cookie(
|
||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
|
||||
return response
|
||||
@@ -522,12 +521,9 @@ def _auth_api_token_response(
|
||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
||||
access_token = create_access_token(data=payload.dict())
|
||||
max_age = settings.auth_token_expire_minutes * 60
|
||||
response = RedirectResponse(path)
|
||||
response.set_cookie(
|
||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
||||
)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
||||
response.set_cookie("cookie_access_token", access_token, httponly=True)
|
||||
response.set_cookie("is_lnbits_user_authorized", "true")
|
||||
response.delete_cookie("is_access_token_expired")
|
||||
return response
|
||||
|
||||
@@ -578,10 +574,10 @@ def _find_auth_provider_class(provider: str) -> Callable:
|
||||
def _nostr_nip98_event(request: Request) -> dict:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr Auth header missing.")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Nostr Auth header missing.")
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "nostr":
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid Authorization scheme.")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid Authorization scheme.")
|
||||
event = None
|
||||
try:
|
||||
event_json = base64.b64decode(token.encode("ascii"))
|
||||
|
||||
@@ -155,7 +155,7 @@ async def api_payments_daily_stats(
|
||||
|
||||
if not user.admin:
|
||||
exc = HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Missing wallet id.",
|
||||
)
|
||||
wallet_filter = next(
|
||||
@@ -264,21 +264,13 @@ async def _api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||
response_description="list of payments",
|
||||
response_model=Page[Payment],
|
||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def api_all_payments_paginated(
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
if user.admin:
|
||||
# admin user can see payments from all wallets
|
||||
for_user_id = None
|
||||
else:
|
||||
# regular user can only see payments from their wallets
|
||||
for_user_id = user.id
|
||||
|
||||
return await get_payments_paginated(
|
||||
filters=filters,
|
||||
user_id=for_user_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -322,7 +314,7 @@ async def api_payments_create(
|
||||
return await _api_payments_create_invoice(invoice_data, wallet.wallet)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Invoice (or Admin) key required.",
|
||||
)
|
||||
|
||||
|
||||
+7
-20
@@ -554,13 +554,9 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
for page_filter in self.filters:
|
||||
where_stmts.append(page_filter.statement)
|
||||
if self.search and self.model and self.model.__search_fields__:
|
||||
# Use `COALESCE` to handle `NULL` values and `||`
|
||||
# for cross-database compatible string concatenation
|
||||
_fields = self.model.__search_fields__
|
||||
search_expr = " || ".join(
|
||||
f"COALESCE(CAST({field} AS TEXT), '')" for field in _fields
|
||||
where_stmts.append(
|
||||
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE :search"
|
||||
)
|
||||
where_stmts.append(f"lower({search_expr}) LIKE :search")
|
||||
|
||||
if where_stmts:
|
||||
return "WHERE " + " AND ".join(where_stmts)
|
||||
@@ -580,7 +576,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
for key, value in page_filter.values.items():
|
||||
values[key] = value
|
||||
if self.search and self.model:
|
||||
values["search"] = f"%{self.search.lower()}%"
|
||||
values["search"] = f"%{self.search}%"
|
||||
return values
|
||||
|
||||
|
||||
@@ -648,9 +644,9 @@ def model_to_dict(model: BaseModel) -> dict:
|
||||
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
||||
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||
if isinstance(value, str):
|
||||
if value == "null" or value == "":
|
||||
if value == "null":
|
||||
return None
|
||||
_subdict = _safe_load_json(value)
|
||||
_subdict = json.loads(value)
|
||||
elif isinstance(value, dict):
|
||||
_subdict = value
|
||||
|
||||
@@ -674,7 +670,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
||||
type_ = model.__fields__[key].type_
|
||||
outertype_ = model.__fields__[key].outer_type_
|
||||
if get_origin(outertype_) is list:
|
||||
_items = _safe_load_json(value) if isinstance(value, str) else value
|
||||
_items = json.loads(value) if isinstance(value, str) else value
|
||||
_dict[key] = [
|
||||
dict_to_submodel(type_, v) if issubclass(type_, BaseModel) else v
|
||||
for v in _items
|
||||
@@ -695,7 +691,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
||||
# TODO: remove this when all sub models are migrated to Pydantic
|
||||
# NOTE: this is for type dict on BaseModel, (used in Payment class)
|
||||
if type_ is dict and value:
|
||||
_dict[key] = _safe_load_json(value)
|
||||
_dict[key] = json.loads(value)
|
||||
continue
|
||||
_dict[key] = value
|
||||
continue
|
||||
@@ -703,12 +699,3 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
||||
if isinstance(_model, BaseModel):
|
||||
_model.__init__(**_dict) # type: ignore
|
||||
return _model
|
||||
|
||||
|
||||
def _safe_load_json(value: str) -> dict:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
# DB is corrupted if it gets here
|
||||
logger.error(f"Failed to decode JSON: '{value}'")
|
||||
return {}
|
||||
|
||||
@@ -102,7 +102,7 @@ class KeyChecker(SecurityBase):
|
||||
request.scope["user_id"] = wallet.user
|
||||
if self.expected_key_type is KeyType.admin and wallet.adminkey != key_value:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="Invalid adminkey.",
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ async def check_user_exists(
|
||||
|
||||
r.scope["user_id"] = account.id
|
||||
if not settings.is_user_allowed(account.id):
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "User not allowed.")
|
||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User not allowed.")
|
||||
|
||||
user = await get_user_from_account(account)
|
||||
if not user:
|
||||
@@ -201,7 +201,7 @@ async def access_token_payload(
|
||||
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "User not authorized. No admin privileges."
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No admin privileges."
|
||||
)
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
@@ -214,7 +214,7 @@ async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User
|
||||
async def check_super_user(user: Annotated[User, Depends(check_user_exists)]) -> User:
|
||||
if user.id != settings.super_user:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, "User not authorized. No super user privileges."
|
||||
HTTPStatus.UNAUTHORIZED, "User not authorized. No super user privileges."
|
||||
)
|
||||
if not user.has_password:
|
||||
raise HTTPException(
|
||||
|
||||
+58
-43
@@ -64,50 +64,63 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
|
||||
window_settings = {
|
||||
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||
"HIDE_API": settings.lnbits_hide_api,
|
||||
"SITE_TITLE": settings.lnbits_site_title,
|
||||
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
||||
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
||||
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
||||
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
||||
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
||||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
|
||||
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
|
||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||
"LNBITS_VERSION": settings.version,
|
||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||
"LNBITS_DENOMINATION": (
|
||||
settings.lnbits_denomination
|
||||
if settings.lnbits_denomination == "FakeWallet"
|
||||
else "sats"
|
||||
),
|
||||
}
|
||||
if settings.lnbits_ad_space_enabled:
|
||||
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
|
||||
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
|
||||
|
||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||
for key, value in window_settings.items():
|
||||
t.env.globals[key] = value
|
||||
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
||||
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
||||
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
||||
t.env.globals["LNBITS_DENOMINATION"] = (
|
||||
settings.lnbits_denomination
|
||||
if settings.lnbits_backend_wallet_class == "FakeWallet"
|
||||
else "sats"
|
||||
)
|
||||
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
|
||||
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
||||
t.env.globals["LNBITS_SHOW_HOME_PAGE_ELEMENTS"] = (
|
||||
settings.lnbits_show_home_page_elements
|
||||
)
|
||||
t.env.globals["LNBITS_CUSTOM_IMAGE"] = settings.lnbits_custom_image
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE"] = settings.lnbits_custom_badge
|
||||
t.env.globals["LNBITS_CUSTOM_BADGE_COLOR"] = settings.lnbits_custom_badge_color
|
||||
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
|
||||
t.env.globals["LNBITS_QR_LOGO"] = settings.lnbits_qr_logo
|
||||
t.env.globals["LNBITS_VERSION"] = settings.version
|
||||
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
||||
t.env.globals["LNBITS_AUTH_METHODS"] = settings.auth_allowed_methods
|
||||
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
||||
t.env.globals["LNBITS_EXTENSIONS_DEACTIVATE_ALL"] = (
|
||||
settings.lnbits_extensions_deactivate_all
|
||||
)
|
||||
t.env.globals["LNBITS_AUDIT_ENABLED"] = settings.lnbits_audit_enabled
|
||||
|
||||
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
|
||||
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
|
||||
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
|
||||
t.env.globals["LNBITS_NODE_UI"] = (
|
||||
settings.lnbits_node_ui and get_node_class() is not None
|
||||
)
|
||||
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
|
||||
t.env.globals["EXTENSIONS"] = list(settings.lnbits_installed_extensions_ids)
|
||||
|
||||
if settings.lnbits_custom_logo:
|
||||
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
||||
|
||||
if settings.lnbits_default_reaction:
|
||||
t.env.globals["USE_DEFAULT_REACTION"] = settings.lnbits_default_reaction
|
||||
|
||||
if settings.lnbits_default_theme:
|
||||
t.env.globals["USE_DEFAULT_THEME"] = settings.lnbits_default_theme
|
||||
|
||||
if settings.lnbits_default_border:
|
||||
t.env.globals["USE_DEFAULT_BORDER"] = settings.lnbits_default_border
|
||||
|
||||
if settings.lnbits_default_gradient:
|
||||
t.env.globals["USE_DEFAULT_GRADIENT"] = settings.lnbits_default_gradient
|
||||
|
||||
if settings.lnbits_default_bgimage:
|
||||
t.env.globals["USE_DEFAULT_BGIMAGE"] = settings.lnbits_default_bgimage
|
||||
|
||||
if settings.bundle_assets:
|
||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||
@@ -121,6 +134,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||
|
||||
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
|
||||
|
||||
return t
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -323,8 +323,6 @@ window.localisation.en = {
|
||||
set_password: 'Set Password',
|
||||
invalid_password: 'Password must have at least 8 characters',
|
||||
invalid_password_repeat: 'Passwords do not match',
|
||||
reset_key_generated: 'A reset key has been generated.',
|
||||
reset_key_copy: 'Click OK to copy the reset URL to your clipboard.',
|
||||
login: 'Login',
|
||||
register: 'Register',
|
||||
username: 'Username',
|
||||
@@ -446,7 +444,7 @@ window.localisation.en = {
|
||||
fee_reserve_percent: 'Fee Reserve Percent',
|
||||
fee_reserve_msats: 'Reserve fee in msats',
|
||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||
payment_wait_time: 'Payment Wait Time (sec)',
|
||||
payment_wait_time: 'Payment Wait Time',
|
||||
payment_wait_time_desc:
|
||||
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
||||
server_management: 'Server Management',
|
||||
|
||||
@@ -100,7 +100,7 @@ window.localisation.fi = {
|
||||
payment_processing: 'Maksua käsitellään...',
|
||||
not_enough_funds: 'Varat eivät riitä!',
|
||||
search_by_tag_memo_amount: 'Etsi tunnisteella, muistiolla tai määrällä',
|
||||
invoice_waiting: 'Lasku odottaa maksua',
|
||||
invoice_waiting: 'Lasku osottaa maksamista',
|
||||
payment_received: 'Maksu vastaanotettu',
|
||||
payment_sent: 'Maksu lähetetty',
|
||||
payment_failed: 'Maksu epäonnistui',
|
||||
@@ -261,7 +261,7 @@ window.localisation.fi = {
|
||||
notification_source: 'Tiedotteiden lähde',
|
||||
notification_source_label:
|
||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||
more: 'näytä lisää',
|
||||
more: 'laajenna',
|
||||
less: 'supista',
|
||||
releases: 'Julkaisut',
|
||||
watchdog: 'Watchdog',
|
||||
@@ -321,8 +321,6 @@ window.localisation.fi = {
|
||||
set_password: 'Aseta salasana',
|
||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||
invalid_password_repeat: 'Salasanat eivät täsmää',
|
||||
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
||||
reset_key_copy: 'Kopioi vaihto-URL leikepöydälle painamalla OK.',
|
||||
login: 'Kirjaudu',
|
||||
register: 'Rekisteröidy',
|
||||
username: 'Käyttäjänimi',
|
||||
|
||||
@@ -450,7 +450,7 @@ window.AdminPageLogic = {
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
formatDate(date) {
|
||||
return moment.utc(date * 1000).fromNow()
|
||||
return moment(date * 1000).fromNow()
|
||||
},
|
||||
sendTestEmail() {
|
||||
LNbits.api
|
||||
@@ -546,10 +546,10 @@ window.AdminPageLogic = {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message:
|
||||
'Success! Restored settings to defaults. Restarting...',
|
||||
'Success! Restored settings to defaults, restart required!',
|
||||
icon: null
|
||||
})
|
||||
this.$q.localStorage.clear()
|
||||
this.needsRestart = true
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
|
||||
@@ -253,12 +253,12 @@ window.LNbits = {
|
||||
}
|
||||
|
||||
obj.date = Quasar.date.formatDate(new Date(obj.time), window.dateFormat)
|
||||
obj.dateFrom = moment.utc(obj.date).fromNow()
|
||||
obj.dateFrom = moment(obj.date).fromNow()
|
||||
obj.expirydate = Quasar.date.formatDate(
|
||||
new Date(obj.expiry),
|
||||
window.dateFormat
|
||||
)
|
||||
obj.expirydateFrom = moment.utc(obj.expirydate).fromNow()
|
||||
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
@@ -495,10 +495,8 @@ window.windowMixin = {
|
||||
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||
: USE_DEFAULT_BGIMAGE,
|
||||
isUserAuthorized: false,
|
||||
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
||||
walletEventListeners: [],
|
||||
backgroundImage: '',
|
||||
...WINDOW_SETTINGS
|
||||
backgroundImage: ''
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ window.app = Vue.createApp({
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.description = this.SITE_DESCRIPTION
|
||||
this.allowedRegister = this.LNBITS_NEW_ACCOUNTS_ALLOWED
|
||||
this.description = SITE_DESCRIPTION
|
||||
this.allowedRegister = ALLOWED_REGISTER
|
||||
this.authAction =
|
||||
!this.allowedRegister ||
|
||||
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
||||
|
||||
@@ -600,7 +600,7 @@ window.app.component('lnbits-date', {
|
||||
return LNbits.utils.formatDate(this.ts)
|
||||
},
|
||||
dateFrom() {
|
||||
return moment.utc(this.date).fromNow()
|
||||
return moment(this.date).fromNow()
|
||||
}
|
||||
},
|
||||
template: `
|
||||
|
||||
@@ -148,7 +148,7 @@ window.PaymentsPageLogic = {
|
||||
if (p.extra && p.extra.tag) {
|
||||
p.tag = p.extra.tag
|
||||
}
|
||||
p.timeFrom = moment.utc(p.created_at).fromNow()
|
||||
p.timeFrom = moment(p.created_at).fromNow()
|
||||
p.outgoing = p.amount < 0
|
||||
p.amount =
|
||||
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
|
||||
@@ -229,10 +229,11 @@ window.PaymentsPageLogic = {
|
||||
'GET',
|
||||
`/api/v1/payments/stats/count?${params}&count_by=status`
|
||||
)
|
||||
|
||||
data.sort((a, b) => a.field - b.field).reverse()
|
||||
|
||||
this.searchOptions.status = data.map(s => s.field)
|
||||
data.sort((a, b) => a.field - b.field)
|
||||
this.searchOptions.status = data
|
||||
.map(s => s.field)
|
||||
.sort()
|
||||
.reverse()
|
||||
this.paymentsStatusChart.data.datasets[0].data = data.map(s => s.total)
|
||||
this.paymentsStatusChart.data.labels = [...this.searchOptions.status]
|
||||
|
||||
@@ -418,8 +419,11 @@ window.PaymentsPageLogic = {
|
||||
data: [],
|
||||
backgroundColor: [
|
||||
'rgb(0, 205, 86)',
|
||||
'rgb(64, 72, 78)',
|
||||
'rgb(255, 99, 132)'
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(255, 5, 86)',
|
||||
'rgb(25, 205, 86)',
|
||||
'rgb(255, 205, 250)'
|
||||
],
|
||||
hoverOffset: 4
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ window.UsersPageLogic = {
|
||||
.then(res => {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
this.$t('reset_key_generated') + ' ' + this.$t('reset_key_copy')
|
||||
'A reset key has been generated. Click OK to copy the rest key to your clipboard.'
|
||||
)
|
||||
.onOk(() => {
|
||||
const url = window.location.origin + '?reset_key=' + res.data
|
||||
|
||||
@@ -261,9 +261,7 @@ window.WalletPageLogic = {
|
||||
this.receive.paymentReq = response.data.bolt11
|
||||
this.receive.amountMsat = response.data.amount
|
||||
this.receive.paymentHash = response.data.payment_hash
|
||||
if (!this.receive.lnurl) {
|
||||
this.readNfcTag()
|
||||
}
|
||||
this.readNfcTag()
|
||||
// TODO: lnurl_callback and lnurl_response
|
||||
// WITHDRAW
|
||||
if (response.data.lnurl_response !== null) {
|
||||
@@ -451,8 +449,8 @@ window.WalletPageLogic = {
|
||||
createdDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expireDateFrom = moment.utc(expireDate).fromNow()
|
||||
cleanInvoice.createdDateFrom = moment.utc(createdDate).fromNow()
|
||||
cleanInvoice.expireDateFrom = moment(expireDate).fromNow()
|
||||
cleanInvoice.createdDateFrom = moment(createdDate).fromNow()
|
||||
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
@@ -1114,6 +1112,13 @@ window.WalletPageLogic = {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.wallet.id'(newVal, oldVal) {
|
||||
try {
|
||||
this.fetchChartData()
|
||||
} catch (error) {
|
||||
console.warn(`Chart creation failed: ${error}`)
|
||||
}
|
||||
},
|
||||
'g.updatePayments'(newVal, oldVal) {
|
||||
this.parse.show = false
|
||||
if (this.receive.paymentHash === this.g.updatePaymentsHash) {
|
||||
|
||||
+1
-2
@@ -203,8 +203,7 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
||||
if payment and payment.is_in:
|
||||
status = await payment.check_status()
|
||||
payment.fee = status.fee_msat or 0
|
||||
# only overwrite preimage if status.preimage provides it
|
||||
payment.preimage = status.preimage or payment.preimage
|
||||
payment.preimage = status.preimage
|
||||
payment.status = PaymentState.SUCCESS
|
||||
await update_payment(payment)
|
||||
internal = "internal" if is_internal else ""
|
||||
|
||||
+30
-26
@@ -390,38 +390,42 @@
|
||||
</q-layout>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||
window[key] = WINDOW_SETTINGS[key]
|
||||
})
|
||||
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
||||
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
||||
}
|
||||
</script>
|
||||
{% include('components.vue') %} {% block vue_templates %}{% endblock %} {%
|
||||
for url in INCLUDED_JS %}
|
||||
<script src="{{ static_url_for('static', url) }}"></script>
|
||||
{% endfor %}
|
||||
<script type="text/javascript">
|
||||
const SITE_DESCRIPTION = {{ SITE_DESCRIPTION | tojson}}
|
||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }}
|
||||
const LNBITS_VERSION = {{ LNBITS_VERSION | tojson }}
|
||||
const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }}
|
||||
const USE_DEFAULT_REACTION = {{ USE_DEFAULT_REACTION | tojson }}
|
||||
const USE_DEFAULT_THEME = {{ USE_DEFAULT_THEME | tojson }}
|
||||
const USE_DEFAULT_BORDER = {{ USE_DEFAULT_BORDER | tojson }}
|
||||
const USE_DEFAULT_GRADIENT = {{ USE_DEFAULT_GRADIENT | lower | tojson }}
|
||||
const USE_DEFAULT_BGIMAGE = "{{ USE_DEFAULT_BGIMAGE or None | tojson }}"
|
||||
if (themes && themes.length) {
|
||||
window.allowedThemes = themes.map(str => str.trim())
|
||||
}
|
||||
window.langs = [
|
||||
{value: 'en', label: 'English', display: '🇬🇧 EN'},
|
||||
{value: 'de', label: 'Deutsch', display: '🇩🇪 DE'},
|
||||
{value: 'es', label: 'Español', display: '🇪🇸 ES'},
|
||||
{value: 'jp', label: '日本語', display: '🇯🇵 JP'},
|
||||
{value: 'cn', label: '中文', display: '🇨🇳 CN'},
|
||||
{value: 'fr', label: 'Français', display: '🇫🇷 FR'},
|
||||
{value: 'it', label: 'Italiano', display: '🇮🇹 IT'},
|
||||
{value: 'pi', label: 'Pirate', display: '🏴☠️ PI'},
|
||||
{value: 'nl', label: 'Nederlands', display: '🇳🇱 NL'},
|
||||
{value: 'we', label: 'Cymraeg', display: '🏴 CY'},
|
||||
{value: 'pl', label: 'Polski', display: '🇵🇱 PL'},
|
||||
{value: 'pt', label: 'Português', display: '🇵🇹 PT'},
|
||||
{value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR'},
|
||||
{value: 'cs', label: 'Česky', display: '🇨🇿 CS'},
|
||||
{value: 'sk', label: 'Slovensky', display: '🇸🇰 SK'},
|
||||
{value: 'kr', label: '한국어', display: '🇰🇷 KR'},
|
||||
{value: 'fi', label: 'Suomi', display: '🇫🇮 FI'}
|
||||
{ value: 'en', label: 'English', display: '🇬🇧 EN' },
|
||||
{ value: 'de', label: 'Deutsch', display: '🇩🇪 DE' },
|
||||
{ value: 'es', label: 'Español', display: '🇪🇸 ES' },
|
||||
{ value: 'jp', label: '日本語', display: '🇯🇵 JP' },
|
||||
{ value: 'cn', label: '中文', display: '🇨🇳 CN' },
|
||||
{ value: 'fr', label: 'Français', display: '🇫🇷 FR' },
|
||||
{ value: 'it', label: 'Italiano', display: '🇮🇹 IT' },
|
||||
{ value: 'pi', label: 'Pirate', display: '🏴☠️ PI' },
|
||||
{ value: 'nl', label: 'Nederlands', display: '🇳🇱 NL' },
|
||||
{ value: 'we', label: 'Cymraeg', display: '🏴 CY' },
|
||||
{ value: 'pl', label: 'Polski', display: '🇵🇱 PL' },
|
||||
{ value: 'pt', label: 'Português', display: '🇵🇹 PT' },
|
||||
{ value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR' },
|
||||
{ value: 'cs', label: 'Česky', display: '🇨🇿 CS' },
|
||||
{ value: 'sk', label: 'Slovensky', display: '🇸🇰 SK' },
|
||||
{ value: 'kr', label: '한국어', display: '🇰🇷 KR' },
|
||||
{ value: 'fi', label: 'Suomi', display: '🇫🇮 FI' }
|
||||
]
|
||||
window.LOCALE = 'en'
|
||||
window.dateFormat = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
+16
-19
@@ -6,7 +6,7 @@
|
||||
>
|
||||
{% block page %}
|
||||
<div class="text-center q-pa-md flex flex-center">
|
||||
<div v-if="statusCode">
|
||||
<div>
|
||||
<div class="error-code" v-text="statusCode"></div>
|
||||
|
||||
<div class="error-message" v-text="message"></div>
|
||||
@@ -65,15 +65,19 @@
|
||||
goToExtension() {
|
||||
window.location.href = `/extensions#${this.extension}`
|
||||
},
|
||||
async logOut() {
|
||||
|
||||
try {
|
||||
await LNbits.api.logout()
|
||||
window.location = '/'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
||||
logOut() {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Do you really want to logout?'
|
||||
)
|
||||
.onOk( async () => {
|
||||
try {
|
||||
await LNbits.api.logout()
|
||||
window.location = '/'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
@@ -82,17 +86,10 @@
|
||||
if (this.message.startsWith('Extension ')) return true
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
created() {
|
||||
this.err = '{{ err }}'
|
||||
const statusCode = '{{ status_code }}' || 404
|
||||
this.statusCode = '{{ status_code }}' || 404
|
||||
this.message = String({{ message | tojson }}) || 'Page not found'
|
||||
if (statusCode == 401) {
|
||||
console.warn(`Unauthorized: ${this.message}`)
|
||||
this.logOut()
|
||||
return
|
||||
}
|
||||
this.statusCode = statusCode
|
||||
|
||||
if (this.isExtension) {
|
||||
this.extension = this.message.match(/'([^']+)'/)[1]
|
||||
}
|
||||
|
||||
@@ -44,20 +44,15 @@
|
||||
</q-layout>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||
window[key] = WINDOW_SETTINGS[key]
|
||||
})
|
||||
if (LNBITS_THEME_OPTIONS && LNBITS_THEME_OPTIONS.length) {
|
||||
window.allowedThemes = LNBITS_THEME_OPTIONS.map(str => str.trim())
|
||||
}
|
||||
</script>
|
||||
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
||||
for url in INCLUDED_JS %}
|
||||
<script src="{{ static_url_for('static', url) }}"></script>
|
||||
{% endfor %} {% block scripts %}{% endblock %} {% for url in
|
||||
INCLUDED_COMPONENTS %}
|
||||
{% endfor %}
|
||||
<script>
|
||||
const LNBITS_QR_LOGO = {{ LNBITS_QR_LOGO | tojson }}
|
||||
</script>
|
||||
<!---->
|
||||
{% block scripts %}{% endblock %} {% for url in INCLUDED_COMPONENTS %}
|
||||
<script src="{{ static_url_for('static', url) }}"></script>
|
||||
{% endfor %}
|
||||
</body>
|
||||
|
||||
+52
-67
@@ -1,13 +1,15 @@
|
||||
from base64 import b64decode, b64encode, urlsafe_b64decode, urlsafe_b64encode
|
||||
import base64
|
||||
import getpass
|
||||
from hashlib import md5, pbkdf2_hmac, sha256
|
||||
from typing import Union
|
||||
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
|
||||
def random_secret_and_hash(length: int = 32) -> tuple[str, str]:
|
||||
secret = Random.new().read(length)
|
||||
|
||||
def random_secret_and_hash() -> tuple[str, str]:
|
||||
secret = Random.new().read(32)
|
||||
return secret.hex(), sha256(secret).hexdigest()
|
||||
|
||||
|
||||
@@ -21,91 +23,74 @@ def fake_privkey(secret: str) -> str:
|
||||
).hex()
|
||||
|
||||
|
||||
def verify_preimage(preimage: str, payment_hash: str) -> bool:
|
||||
preimage_bytes = bytes.fromhex(preimage)
|
||||
calculated_hash = sha256(preimage_bytes).hexdigest()
|
||||
return calculated_hash == payment_hash
|
||||
|
||||
|
||||
class AESCipher:
|
||||
"""
|
||||
AES-256-CBC encryption/decryption with salt and base64 encoding.
|
||||
:param key: The key to use for en-/decryption. It can be bytes, a hex or a string.
|
||||
"""This class is compatible with crypto-js/aes.js
|
||||
|
||||
This class is compatible with crypto-js/aes.js
|
||||
Encrypt and decrypt in Javascript using:
|
||||
import AES from "crypto-js/aes.js";
|
||||
import Utf8 from "crypto-js/enc-utf8.js";
|
||||
AES.encrypt(decrypted, password).toString()
|
||||
AES.decrypt(encrypted, password).toString(Utf8);
|
||||
import AES from "crypto-js/aes.js";
|
||||
import Utf8 from "crypto-js/enc-utf8.js";
|
||||
AES.encrypt(decrypted, password).toString()
|
||||
AES.decrypt(encrypted, password).toString(Utf8);
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, key: Union[bytes, str], block_size: int = 16):
|
||||
self.block_size = block_size
|
||||
if isinstance(key, bytes):
|
||||
self.key = key
|
||||
return
|
||||
try:
|
||||
self.key = bytes.fromhex(key)
|
||||
except ValueError:
|
||||
pass
|
||||
self.key = key.encode()
|
||||
def __init__(self, key=None, description=""):
|
||||
self.key = key
|
||||
self.description = description + " "
|
||||
|
||||
def pad(self, data: bytes) -> bytes:
|
||||
length = self.block_size - (len(data) % self.block_size)
|
||||
def pad(self, data):
|
||||
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||
return data + (chr(length) * length).encode()
|
||||
|
||||
def unpad(self, data: bytes) -> bytes:
|
||||
_last = data[-1]
|
||||
if isinstance(_last, int):
|
||||
return data[:-_last]
|
||||
return data[: -ord(_last)]
|
||||
def unpad(self, data):
|
||||
return data[: -(data[-1] if isinstance(data[-1], int) else ord(data[-1]))]
|
||||
|
||||
def derive_iv_and_key(
|
||||
self, salt: bytes, output_len: int = 32 + 16
|
||||
) -> tuple[bytes, bytes]:
|
||||
@property
|
||||
def passphrase(self):
|
||||
passphrase = self.key if self.key is not None else None
|
||||
if passphrase is None:
|
||||
passphrase = getpass.getpass(f"Enter {self.description}password:")
|
||||
return passphrase
|
||||
|
||||
def bytes_to_key(self, data, salt, output=48):
|
||||
# extended from https://gist.github.com/gsakkis/4546068
|
||||
assert len(salt) == 8, "Salt must be 8 bytes"
|
||||
data = self.key + salt
|
||||
assert len(salt) == 8, len(salt)
|
||||
data += salt
|
||||
key = md5(data).digest()
|
||||
final_key = key
|
||||
while len(final_key) < output_len:
|
||||
while len(final_key) < output:
|
||||
key = md5(key + data).digest()
|
||||
final_key += key
|
||||
iv_key = final_key[:output_len]
|
||||
return iv_key[32:], iv_key[:32]
|
||||
return final_key[:output]
|
||||
|
||||
def decrypt(self, encrypted: str, urlsafe: bool = False) -> str:
|
||||
"""Decrypts a salted base64 encoded string using AES-256-CBC."""
|
||||
"""Decrypts a string using AES-256-CBC."""
|
||||
passphrase = self.passphrase
|
||||
|
||||
if urlsafe:
|
||||
decoded = urlsafe_b64decode(encrypted)
|
||||
encrypted_bytes = base64.urlsafe_b64decode(encrypted)
|
||||
else:
|
||||
decoded = b64decode(encrypted)
|
||||
encrypted_bytes = base64.b64decode(encrypted)
|
||||
|
||||
if decoded[0:8] != b"Salted__":
|
||||
raise ValueError("Invalid salt.")
|
||||
|
||||
salt = decoded[8:16]
|
||||
encrypted_bytes = decoded[16:]
|
||||
|
||||
iv, key = self.derive_iv_and_key(salt, 32 + 16)
|
||||
assert encrypted_bytes[0:8] == b"Salted__"
|
||||
salt = encrypted_bytes[8:16]
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
|
||||
try:
|
||||
decrypted_bytes = aes.decrypt(encrypted_bytes)
|
||||
return self.unpad(decrypted_bytes).decode()
|
||||
except Exception as exc:
|
||||
raise ValueError("Decryption error") from exc
|
||||
return self.unpad(aes.decrypt(encrypted_bytes[16:])).decode()
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ValueError("Wrong passphrase") from exc
|
||||
|
||||
def encrypt(self, message: bytes, urlsafe: bool = False) -> str:
|
||||
"""
|
||||
Encrypts a string using AES-256-CBC and returns a salted base64 encoded string.
|
||||
"""
|
||||
passphrase = self.passphrase
|
||||
salt = Random.new().read(8)
|
||||
iv, key = self.derive_iv_and_key(salt, 32 + 16)
|
||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||
key = key_iv[:32]
|
||||
iv = key_iv[32:]
|
||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||
msg = self.pad(message)
|
||||
encrypted = aes.encrypt(msg)
|
||||
salted = b"Salted__" + salt + encrypted
|
||||
encoded = urlsafe_b64encode(salted) if urlsafe else b64encode(salted)
|
||||
return encoded.decode()
|
||||
encoded = b"Salted__" + salt + aes.encrypt(self.pad(message))
|
||||
return (
|
||||
base64.urlsafe_b64encode(encoded) if urlsafe else base64.b64encode(encoded)
|
||||
).decode()
|
||||
|
||||
+14
-21
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from typing import AsyncGenerator, Optional
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -72,10 +72,10 @@ class AlbyWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://api.getalby.com/invoices
|
||||
data: dict = {"amount": f"{amount}"}
|
||||
data: Dict = {"amount": f"{amount}"}
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash.hex()
|
||||
elif unhashed_description:
|
||||
@@ -95,31 +95,25 @@ class AlbyWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = data["message"] if "message" in data else r.text
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
payment_request = data["payment_request"]
|
||||
preimage = data.get("payment_preimage")
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -135,31 +129,30 @@ class AlbyWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = data["message"] if "message" in data else r.text
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
# todo: confirm with bitkarrot that having the minus is fine
|
||||
# other funding sources return a positive fee value
|
||||
fee_msat = -data["fee"]
|
||||
preimage = data["payment_preimage"]
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
@@ -20,7 +20,6 @@ class InvoiceResponse(NamedTuple):
|
||||
checking_id: str | None = None # payment_hash, rpc_id
|
||||
payment_request: str | None = None
|
||||
error_message: str | None = None
|
||||
preimage: str | None = None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
|
||||
+7
-12
@@ -135,7 +135,7 @@ class BlinkWallet(Wallet):
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
payment_request = (
|
||||
response.get("data", {})
|
||||
@@ -150,18 +150,15 @@ class BlinkWallet(Wallet):
|
||||
.get("paymentHash", None)
|
||||
)
|
||||
|
||||
# TODO: add preimage to response
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(
|
||||
@@ -188,21 +185,19 @@ class BlinkWallet(Wallet):
|
||||
)
|
||||
if len(errors) > 0:
|
||||
error_message = errors[0].get("message")
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
|
||||
payment_status = await self.get_payment_status(checking_id)
|
||||
fee_msat = payment_status.fee_msat
|
||||
preimage = payment_status.preimage
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
+14
-12
@@ -182,16 +182,15 @@ else:
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: add preimage
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=breez_invoice.ln_invoice.payment_hash,
|
||||
payment_request=breez_invoice.ln_invoice.bolt11,
|
||||
# preimage=breez_invoice.ln_invoice.payment_preimage,
|
||||
True,
|
||||
breez_invoice.ln_invoice.payment_hash,
|
||||
breez_invoice.ln_invoice.bolt11,
|
||||
None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return InvoiceResponse(ok=False, error_message=str(e))
|
||||
return InvoiceResponse(False, None, None, str(e))
|
||||
|
||||
async def pay_invoice(
|
||||
self, bolt11: str, fee_limit_msat: int
|
||||
@@ -218,19 +217,22 @@ else:
|
||||
except Exception as ex:
|
||||
logger.info(ex)
|
||||
# assume that payment failed?
|
||||
return PaymentResponse(ok=False, error_message=f"payment failed: {exc}")
|
||||
return PaymentResponse(
|
||||
False, None, None, None, f"payment failed: {exc}"
|
||||
)
|
||||
|
||||
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
|
||||
return PaymentResponse(ok=False, error_message="payment is pending")
|
||||
return PaymentResponse(False, None, None, None, "payment is pending")
|
||||
|
||||
# let's use the payment_hash as the checking_id
|
||||
checking_id = invoice.payment_hash
|
||||
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
fee_msat=payment.fee_msat,
|
||||
preimage=payment.details.data.payment_preimage,
|
||||
True,
|
||||
checking_id,
|
||||
payment.fee_msat,
|
||||
payment.details.data.payment_preimage,
|
||||
None,
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
+12
-16
@@ -54,7 +54,7 @@ class ClicheWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
if unhashed_description or description_hash:
|
||||
description_hash_str = (
|
||||
@@ -79,13 +79,12 @@ class ClicheWallet(Wallet):
|
||||
data = json.loads(r)
|
||||
checking_id = None
|
||||
payment_request = None
|
||||
error_message = None
|
||||
|
||||
if data.get("error") is not None and data["error"].get("message"):
|
||||
logger.error(data["error"]["message"])
|
||||
error_message = data["error"]["message"]
|
||||
return InvoiceResponse(
|
||||
ok=False, checking_id=checking_id, error_message=error_message
|
||||
)
|
||||
return InvoiceResponse(False, checking_id, payment_request, error_message)
|
||||
|
||||
if data.get("result") is not None:
|
||||
checking_id, payment_request = (
|
||||
@@ -93,19 +92,15 @@ class ClicheWallet(Wallet):
|
||||
data["result"]["invoice"],
|
||||
)
|
||||
else:
|
||||
return InvoiceResponse(ok=False, error_message="Could not get payment hash")
|
||||
return InvoiceResponse(False, None, None, "Could not get payment hash")
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=data["result"].get("preimage"),
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
ws = create_connection(self.endpoint)
|
||||
ws.send(f"pay-invoice --invoice {bolt11}")
|
||||
checking_id, fee_msat, preimage, payment_ok = (
|
||||
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -114,7 +109,8 @@ class ClicheWallet(Wallet):
|
||||
for _ in range(2):
|
||||
r = ws.recv()
|
||||
data = json.loads(r)
|
||||
checking_id, fee_msat, preimage, payment_ok = (
|
||||
checking_id, fee_msat, preimage, error_message, payment_ok = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -123,7 +119,7 @@ class ClicheWallet(Wallet):
|
||||
|
||||
if data.get("error") is not None:
|
||||
error_message = data["error"].get("message")
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
if data.get("method") == "payment_succeeded":
|
||||
payment_ok = True
|
||||
@@ -133,10 +129,10 @@ class ClicheWallet(Wallet):
|
||||
continue
|
||||
|
||||
if data.get("result") is None:
|
||||
return PaymentResponse(error_message="result is None")
|
||||
return PaymentResponse(None)
|
||||
|
||||
return PaymentResponse(
|
||||
ok=payment_ok, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
payment_ok, checking_id, fee_msat, preimage, error_message
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import random
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, Optional
|
||||
from typing import Any, AsyncGenerator, Optional
|
||||
|
||||
from bolt11.decode import decode as bolt11_decode
|
||||
from bolt11.exceptions import Bolt11Exception
|
||||
@@ -10,7 +9,6 @@ from pyln.client import LightningRpc, RpcError
|
||||
|
||||
from lnbits.nodes.cln import CoreLightningNode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
@@ -102,11 +100,6 @@ class CoreLightningWallet(Wallet):
|
||||
)
|
||||
if unhashed_description and not self.supports_description_hash:
|
||||
raise UnsupportedError("unhashed_description")
|
||||
|
||||
preimage = kwargs.get("preimage")
|
||||
if not preimage:
|
||||
preimage, _ = random_secret_and_hash()
|
||||
|
||||
r: dict = self.ln.invoice( # type: ignore
|
||||
amount_msat=msat,
|
||||
label=label,
|
||||
@@ -114,7 +107,6 @@ class CoreLightningWallet(Wallet):
|
||||
unhashed_description.decode() if unhashed_description else memo
|
||||
),
|
||||
exposeprivatechannels=True,
|
||||
preimage=preimage,
|
||||
deschashonly=(
|
||||
True if unhashed_description else False
|
||||
), # we can't pass None here
|
||||
@@ -123,39 +115,35 @@ class CoreLightningWallet(Wallet):
|
||||
|
||||
if r.get("code") and r.get("code") < 0: # type: ignore
|
||||
raise Exception(r.get("message"))
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=r["payment_hash"],
|
||||
payment_request=r["bolt11"],
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
return InvoiceResponse(True, r["payment_hash"], r["bolt11"], None)
|
||||
except RpcError as exc:
|
||||
logger.warning(exc)
|
||||
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return InvoiceResponse(ok=False, error_message=str(e))
|
||||
return InvoiceResponse(False, None, None, str(e))
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
try:
|
||||
invoice = bolt11_decode(bolt11)
|
||||
except Bolt11Exception as exc:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
try:
|
||||
previous_payment = await self.get_payment_status(invoice.payment_hash)
|
||||
if previous_payment.paid:
|
||||
return PaymentResponse(ok=False, error_message="invoice already paid")
|
||||
return PaymentResponse(False, None, None, None, "invoice already paid")
|
||||
|
||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||
return PaymentResponse(
|
||||
ok=False, error_message="CLN 0 amount invoice not supported"
|
||||
False, None, None, None, "CLN 0 amount invoice not supported"
|
||||
)
|
||||
|
||||
# maxfee overrides both maxfeepercent and exemptfee defaults (and
|
||||
@@ -182,23 +170,23 @@ class CoreLightningWallet(Wallet):
|
||||
if error_code in self.pay_failure_error_codes:
|
||||
error_message = exc.error.get("message", error_code) # type: ignore
|
||||
return PaymentResponse(
|
||||
ok=False, error_message=f"Payment failed: {error_message}"
|
||||
False, None, None, None, f"Payment failed: {error_message}"
|
||||
)
|
||||
else:
|
||||
error_message = f"Payment failed: {exc.error}"
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
except Exception:
|
||||
error_message = f"RPC '{exc.method}' failed with '{exc.error}'."
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message=f"Payment failed: '{exc}'.")
|
||||
return PaymentResponse(None, None, None, None, f"Payment failed: '{exc}'.")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Optional
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import Bolt11Exception
|
||||
@@ -10,7 +9,6 @@ from bolt11.decode import decode
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
@@ -111,7 +109,7 @@ class CoreLightningRestWallet(Wallet):
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
label = kwargs.get("label", f"lbl{random.random()}")
|
||||
data: dict = {
|
||||
data: Dict = {
|
||||
"amount": amount * 1000,
|
||||
"description": memo,
|
||||
"label": label,
|
||||
@@ -128,10 +126,8 @@ class CoreLightningRestWallet(Wallet):
|
||||
if kwargs.get("expiry"):
|
||||
data["expiry"] = kwargs["expiry"]
|
||||
|
||||
preimage, _ = random_secret_and_hash()
|
||||
|
||||
# https://github.com/Ride-The-Lightning/c-lightning-REST/blob/master/controllers/invoice.js#L52C17-L52C25
|
||||
data["preimage"] = preimage
|
||||
if kwargs.get("preimage"):
|
||||
data["preimage"] = kwargs["preimage"]
|
||||
|
||||
try:
|
||||
r = await self.client.post(
|
||||
@@ -143,48 +139,41 @@ class CoreLightningRestWallet(Wallet):
|
||||
data = r.json()
|
||||
|
||||
if len(data) == 0:
|
||||
return InvoiceResponse(ok=False, error_message="no data")
|
||||
return InvoiceResponse(False, None, None, "no data")
|
||||
|
||||
if "error" in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{r.text}'"
|
||||
)
|
||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
||||
|
||||
if "payment_hash" not in data or "bolt11" not in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=data["payment_hash"],
|
||||
payment_request=data["bolt11"],
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.url}."
|
||||
False, None, None, f"Unable to connect to {self.url}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
try:
|
||||
invoice = decode(bolt11)
|
||||
except Bolt11Exception as exc:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
if not invoice.amount_msat or invoice.amount_msat <= 0:
|
||||
error_message = "0 amount invoices are not allowed"
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
try:
|
||||
r = await self.client.post(
|
||||
f"{self.url}/v1/pay",
|
||||
@@ -201,16 +190,18 @@ class CoreLightningRestWallet(Wallet):
|
||||
status = self.statuses.get(data["status"])
|
||||
if "payment_preimage" not in data:
|
||||
return PaymentResponse(
|
||||
ok=status, error_message=data.get("error") or "unknown error"
|
||||
status,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
data.get("error"),
|
||||
)
|
||||
|
||||
checking_id = data["payment_hash"]
|
||||
preimage = data["payment_preimage"]
|
||||
fee_msat = data["msatoshi_sent"] - data["msatoshi"]
|
||||
|
||||
return PaymentResponse(
|
||||
ok=status, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(status, checking_id, fee_msat, preimage, None)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
try:
|
||||
logger.debug(exc)
|
||||
@@ -218,26 +209,28 @@ class CoreLightningRestWallet(Wallet):
|
||||
error_code = int(data["error"]["code"])
|
||||
if error_code in self.pay_failure_error_codes:
|
||||
error_message = f"Payment failed: {data['error']['message']}"
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
error_message = f"REST failed with {data['error']['message']}."
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
except Exception as exc:
|
||||
error_message = f"Unable to connect to {self.url}."
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message=f"Unable to connect to {self.url}.")
|
||||
return PaymentResponse(
|
||||
None, None, None, None, f"Unable to connect to {self.url}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(
|
||||
|
||||
+19
-30
@@ -3,14 +3,13 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import urllib.parse
|
||||
from typing import Any, AsyncGenerator, Optional
|
||||
from typing import Any, AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from websockets.client import connect
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
@@ -86,7 +85,7 @@ class EclairWallet(Wallet):
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: dict[str, Any] = {
|
||||
data: Dict[str, Any] = {
|
||||
"amountMsat": amount * 1000,
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
@@ -100,45 +99,36 @@ class EclairWallet(Wallet):
|
||||
else:
|
||||
data["description"] = memo
|
||||
|
||||
preimage, _ = random_secret_and_hash()
|
||||
data["paymentPreimage"] = preimage
|
||||
|
||||
try:
|
||||
r = await self.client.post("/createinvoice", data=data, timeout=40)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if len(data) == 0:
|
||||
return InvoiceResponse(ok=False, error_message="no data")
|
||||
return InvoiceResponse(False, None, None, "no data")
|
||||
|
||||
if "error" in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{r.text}'"
|
||||
)
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=data["paymentHash"],
|
||||
payment_request=data["serialized"],
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
||||
|
||||
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.url}."
|
||||
False, None, None, f"Unable to connect to {self.url}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -152,36 +142,35 @@ class EclairWallet(Wallet):
|
||||
data = r.json()
|
||||
|
||||
if "error" in data:
|
||||
return PaymentResponse(error_message=data["error"])
|
||||
return PaymentResponse(None, None, None, None, data["error"])
|
||||
if r.is_error:
|
||||
return PaymentResponse(error_message=r.text)
|
||||
return PaymentResponse(None, None, None, None, r.text)
|
||||
|
||||
if data["type"] == "payment-failed":
|
||||
return PaymentResponse(ok=False, error_message="payment failed")
|
||||
return PaymentResponse(False, None, None, None, "payment failed")
|
||||
|
||||
checking_id = data["paymentHash"]
|
||||
preimage = data["paymentPreimage"]
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message=f"Unable to connect to {self.url}.")
|
||||
return PaymentResponse(
|
||||
None, None, None, None, f"Unable to connect to {self.url}."
|
||||
)
|
||||
|
||||
payment_status: PaymentStatus = await self.get_payment_status(checking_id)
|
||||
success = True if payment_status.success else None
|
||||
return PaymentResponse(
|
||||
ok=success,
|
||||
checking_id=checking_id,
|
||||
fee_msat=payment_status.fee_msat,
|
||||
preimage=preimage,
|
||||
success, checking_id, payment_status.fee_msat, preimage, None
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
@@ -100,7 +100,7 @@ class FakeWallet(Wallet):
|
||||
ok=True,
|
||||
checking_id=payment_hash,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage.hex(),
|
||||
# preimage=preimage.hex(),
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||
|
||||
+12
-20
@@ -92,28 +92,23 @@ class LNbitsWallet(Wallet):
|
||||
if r.is_error or not payment_str:
|
||||
error_message = data["detail"] if "detail" in data else r.text
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{error_message}'"
|
||||
False, None, None, f"Server error: '{error_message}'"
|
||||
)
|
||||
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=data["checking_id"],
|
||||
payment_request=payment_str,
|
||||
preimage=data.get("preimage"),
|
||||
)
|
||||
return InvoiceResponse(True, data["checking_id"], payment_str, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -134,10 +129,7 @@ class LNbitsWallet(Wallet):
|
||||
|
||||
success = True if payment.success else None
|
||||
return PaymentResponse(
|
||||
ok=success,
|
||||
checking_id=checking_id,
|
||||
fee_msat=payment.fee_msat,
|
||||
preimage=payment.preimage,
|
||||
success, checking_id, payment.fee_msat, payment.preimage
|
||||
)
|
||||
|
||||
except httpx.HTTPStatusError as exc:
|
||||
@@ -146,25 +138,25 @@ class LNbitsWallet(Wallet):
|
||||
data = exc.response.json()
|
||||
error_message = f"Payment {data['status']}: {data['detail']}."
|
||||
if data["status"] == "failed":
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
except Exception as exc:
|
||||
error_message = f"Unable to connect to {self.endpoint}."
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
+32
-50
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import base64
|
||||
from hashlib import sha256
|
||||
import hashlib
|
||||
from os import environ
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
@@ -12,11 +12,10 @@ import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
||||
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
@@ -72,11 +71,6 @@ class LndWallet(Wallet):
|
||||
"cannot initialize LndWallet: missing lnd_grpc_cert or lnd_cert"
|
||||
)
|
||||
|
||||
self.endpoint = self.normalize_endpoint(
|
||||
settings.lnd_grpc_endpoint, add_proto=False
|
||||
)
|
||||
self.port = int(settings.lnd_grpc_port)
|
||||
|
||||
macaroon = (
|
||||
settings.lnd_grpc_macaroon
|
||||
or settings.lnd_grpc_admin_macaroon
|
||||
@@ -85,11 +79,23 @@ class LndWallet(Wallet):
|
||||
or settings.lnd_invoice_macaroon
|
||||
)
|
||||
encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted
|
||||
try:
|
||||
self.macaroon = load_macaroon(macaroon, encrypted_macaroon)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"cannot load macaroon for LndWallet: {exc!s}") from exc
|
||||
if encrypted_macaroon:
|
||||
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
||||
encrypted_macaroon
|
||||
)
|
||||
if not macaroon:
|
||||
raise ValueError(
|
||||
"cannot initialize LndWallet: "
|
||||
"missing lnd_grpc_macaroon or lnd_grpc_admin_macaroon or "
|
||||
"lnd_admin_macaroon or lnd_grpc_invoice_macaroon or "
|
||||
"lnd_invoice_macaroon or lnd_grpc_macaroon_encrypted"
|
||||
)
|
||||
|
||||
self.endpoint = self.normalize_endpoint(
|
||||
settings.lnd_grpc_endpoint, add_proto=False
|
||||
)
|
||||
self.port = int(settings.lnd_grpc_port)
|
||||
self.macaroon = load_macaroon(macaroon)
|
||||
cert = open(cert_path, "rb").read()
|
||||
creds = grpc.ssl_channel_credentials(cert)
|
||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
||||
@@ -133,38 +139,21 @@ class LndWallet(Wallet):
|
||||
if description_hash:
|
||||
data["description_hash"] = description_hash
|
||||
elif unhashed_description:
|
||||
data["description_hash"] = sha256(unhashed_description).digest()
|
||||
data["description_hash"] = hashlib.sha256(
|
||||
unhashed_description
|
||||
).digest() # as bytes directly
|
||||
|
||||
preimage = kwargs.get("preimage")
|
||||
if preimage:
|
||||
payment_hash = sha256(preimage.encode()).hexdigest()
|
||||
else:
|
||||
preimage, payment_hash = random_secret_and_hash()
|
||||
|
||||
data["r_hash"] = bytes.fromhex(payment_hash)
|
||||
data["r_preimage"] = bytes.fromhex(preimage)
|
||||
try:
|
||||
req = ln.Invoice(**data)
|
||||
resp = await self.rpc.AddInvoice(req)
|
||||
# response model
|
||||
# {
|
||||
# "r_hash": <bytes>,
|
||||
# "payment_request": <string>,
|
||||
# "add_index": <uint64>,
|
||||
# "payment_addr": <bytes>,
|
||||
# }
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||
error_message = str(exc)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
checking_id = bytes_to_hex(resp.r_hash)
|
||||
payment_request = str(resp.payment_request)
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||
@@ -178,7 +167,7 @@ class LndWallet(Wallet):
|
||||
resp = await self.routerpc.SendPaymentV2(req).read()
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(error_message=str(exc))
|
||||
return PaymentResponse(None, None, None, None, str(exc))
|
||||
|
||||
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
||||
statuses = {
|
||||
@@ -206,18 +195,12 @@ class LndWallet(Wallet):
|
||||
fee_msat = -resp.htlcs[-1].route.total_fees_msat
|
||||
preimage = resp.payment_preimage
|
||||
checking_id = resp.payment_hash
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
elif statuses[resp.status] is False:
|
||||
error_message = failure_reasons[resp.failure_reason]
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
else:
|
||||
return PaymentResponse(
|
||||
ok=None,
|
||||
checking_id=checking_id,
|
||||
error_message="Payment in flight or non-existant.",
|
||||
)
|
||||
|
||||
return PaymentResponse(
|
||||
statuses[resp.status], checking_id, fee_msat, preimage, error_message
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
@@ -228,11 +211,10 @@ class LndWallet(Wallet):
|
||||
raise ValueError
|
||||
|
||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
||||
if resp.settled:
|
||||
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
|
||||
|
||||
if resp.state == "CANCELED":
|
||||
return PaymentFailedStatus()
|
||||
# todo: where is the FAILED status
|
||||
if resp.settled:
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
return PaymentPendingStatus()
|
||||
except grpc.RpcError as exc:
|
||||
|
||||
+46
-65
@@ -9,7 +9,7 @@ from loguru import logger
|
||||
|
||||
from lnbits.nodes.lndrest import LndRestNode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import random_secret_and_hash
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
@@ -35,6 +35,26 @@ class LndRestWallet(Wallet):
|
||||
"cannot initialize LndRestWallet: missing lnd_rest_endpoint"
|
||||
)
|
||||
|
||||
macaroon = (
|
||||
settings.lnd_rest_macaroon
|
||||
or settings.lnd_admin_macaroon
|
||||
or settings.lnd_rest_admin_macaroon
|
||||
or settings.lnd_invoice_macaroon
|
||||
or settings.lnd_rest_invoice_macaroon
|
||||
)
|
||||
encrypted_macaroon = settings.lnd_rest_macaroon_encrypted
|
||||
if encrypted_macaroon:
|
||||
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
||||
encrypted_macaroon
|
||||
)
|
||||
if not macaroon:
|
||||
raise ValueError(
|
||||
"cannot initialize LndRestWallet: "
|
||||
"missing lnd_rest_macaroon or lnd_admin_macaroon or "
|
||||
"lnd_rest_admin_macaroon or lnd_invoice_macaroon or "
|
||||
"lnd_rest_invoice_macaroon or lnd_rest_macaroon_encrypted"
|
||||
)
|
||||
|
||||
if not settings.lnd_rest_cert:
|
||||
logger.warning(
|
||||
"No certificate for LndRestWallet provided! "
|
||||
@@ -48,21 +68,7 @@ class LndRestWallet(Wallet):
|
||||
# even on startup
|
||||
cert = settings.lnd_rest_cert or True
|
||||
|
||||
macaroon = (
|
||||
settings.lnd_rest_macaroon
|
||||
or settings.lnd_admin_macaroon
|
||||
or settings.lnd_rest_admin_macaroon
|
||||
or settings.lnd_invoice_macaroon
|
||||
or settings.lnd_rest_invoice_macaroon
|
||||
)
|
||||
encrypted_macaroon = settings.lnd_rest_macaroon_encrypted
|
||||
try:
|
||||
macaroon = load_macaroon(macaroon, encrypted_macaroon)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"cannot load macaroon for LndRestWallet: {exc!s}"
|
||||
) from exc
|
||||
|
||||
macaroon = load_macaroon(macaroon)
|
||||
headers = {
|
||||
"Grpc-Metadata-macaroon": macaroon,
|
||||
"User-Agent": settings.user_agent,
|
||||
@@ -104,67 +110,56 @@ class LndRestWallet(Wallet):
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
_data: Dict = {
|
||||
data: Dict = {
|
||||
"value": amount,
|
||||
"private": settings.lnd_rest_route_hints,
|
||||
"memo": memo or "",
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
_data["expiry"] = kwargs["expiry"]
|
||||
data["expiry"] = kwargs["expiry"]
|
||||
if description_hash:
|
||||
_data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||
data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||
"ascii"
|
||||
)
|
||||
elif unhashed_description:
|
||||
_data["description_hash"] = base64.b64encode(
|
||||
data["description_hash"] = base64.b64encode(
|
||||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
|
||||
preimage, _payment_hash = random_secret_and_hash()
|
||||
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode()
|
||||
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
|
||||
|
||||
try:
|
||||
r = await self.client.post(url="/v1/invoices", json=_data)
|
||||
r = await self.client.post(url="/v1/invoices", json=data)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if len(data) == 0:
|
||||
return InvoiceResponse(ok=False, error_message="no data")
|
||||
return InvoiceResponse(False, None, None, "no data")
|
||||
|
||||
if "error" in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"""Server error: '{data["error"]}'"""
|
||||
False, None, None, f"""Server error: '{data["error"]}'"""
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{r.text}'"
|
||||
)
|
||||
return InvoiceResponse(False, None, None, f"Server error: '{r.text}'")
|
||||
|
||||
if "payment_request" not in data or "r_hash" not in data:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
|
||||
payment_request = data["payment_request"]
|
||||
payment_hash = base64.b64decode(data["r_hash"]).hex()
|
||||
checking_id = payment_hash
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -190,27 +185,25 @@ class LndRestWallet(Wallet):
|
||||
payment_error = data.get("payment_error")
|
||||
if payment_error:
|
||||
logger.warning(f"LndRestWallet payment_error: {payment_error}.")
|
||||
return PaymentResponse(ok=False, error_message=payment_error)
|
||||
return PaymentResponse(False, None, None, None, payment_error)
|
||||
|
||||
checking_id = base64.b64decode(data["payment_hash"]).hex()
|
||||
fee_msat = int(data["payment_route"]["total_fees_msat"])
|
||||
preimage = base64.b64decode(data["payment_preimage"]).hex()
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"LndRestWallet pay_invoice POST error: {exc}.")
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
@@ -219,22 +212,15 @@ class LndRestWallet(Wallet):
|
||||
try:
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
if r.is_error or not data.get("settled"):
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
# it will return an error and no "settled" attribute on the object
|
||||
return PaymentPendingStatus()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice status: {e}")
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if r.is_error or data.get("settled") is None:
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
# it will return an error and no "settled" attribute on the object
|
||||
return PaymentPendingStatus()
|
||||
|
||||
if data.get("settled") is True:
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
if data.get("state") == "CANCELED":
|
||||
return PaymentFailedStatus()
|
||||
|
||||
return PaymentPendingStatus()
|
||||
return PaymentSuccessStatus()
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
"""
|
||||
@@ -280,12 +266,7 @@ class LndRestWallet(Wallet):
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
# API returns fee_msat as string, explicitly convert to int
|
||||
fee_msat=(
|
||||
int(payment["fee_msat"])
|
||||
if payment.get("fee_msat")
|
||||
else None
|
||||
),
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
|
||||
+18
-15
@@ -91,18 +91,21 @@ class LNPayWallet(Wallet):
|
||||
json=data,
|
||||
timeout=60,
|
||||
)
|
||||
if r.status_code == 201:
|
||||
data = r.json()
|
||||
self.pending_invoices.append(data["id"])
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
payment_request=data["payment_request"],
|
||||
)
|
||||
return InvoiceResponse(
|
||||
ok=False,
|
||||
error_message=r.text,
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
r.status_code == 201,
|
||||
None,
|
||||
None,
|
||||
r.text,
|
||||
)
|
||||
|
||||
if ok:
|
||||
data = r.json()
|
||||
checking_id, payment_request = data["id"], data["payment_request"]
|
||||
|
||||
self.pending_invoices.append(checking_id)
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/withdraw",
|
||||
@@ -113,17 +116,17 @@ class LNPayWallet(Wallet):
|
||||
try:
|
||||
data = r.json()
|
||||
except Exception:
|
||||
return PaymentResponse(ok=False, error_message="Got invalid JSON.")
|
||||
return PaymentResponse(
|
||||
False, None, 0, None, f"Got invalid JSON: {r.text[:200]}"
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return PaymentResponse(ok=False, error_message=data["message"])
|
||||
return PaymentResponse(False, None, None, None, data["message"])
|
||||
|
||||
checking_id = data["lnTx"]["id"]
|
||||
fee_msat = 0
|
||||
preimage = data["lnTx"]["payment_preimage"]
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
return await self.get_payment_status(checking_id)
|
||||
|
||||
@@ -70,7 +70,7 @@ class LnTipsWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
data: Dict = {"amount": amount, "description_hash": "", "memo": memo or ""}
|
||||
if description_hash:
|
||||
@@ -91,14 +91,11 @@ class LnTipsWallet(Wallet):
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
data = r.json()
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=data["payment_hash"],
|
||||
payment_request=data["payment_request"],
|
||||
preimage=data.get("preimage"),
|
||||
True, data["payment_hash"], data["payment_request"], None
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -108,7 +105,7 @@ class LnTipsWallet(Wallet):
|
||||
timeout=None,
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentResponse(ok=False, error_message=r.text)
|
||||
return PaymentResponse(False, None, 0, None, r.text)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
@@ -116,15 +113,13 @@ class LnTipsWallet(Wallet):
|
||||
error_message = data["error"]
|
||||
except Exception:
|
||||
error_message = r.text
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, 0, None, error_message)
|
||||
|
||||
data = r.json()["details"]
|
||||
checking_id = data["payment_hash"]
|
||||
fee_msat = -data["fee"]
|
||||
preimage = data["preimage"]
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import base64
|
||||
from getpass import getpass
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
|
||||
|
||||
def load_macaroon(
|
||||
macaroon: Optional[str] = None,
|
||||
encrypted_macaroon: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Returns hex version of a macaroon encoded in base64 or the file path."""
|
||||
def load_macaroon(macaroon: str) -> str:
|
||||
"""Returns hex version of a macaroon encoded in base64 or the file path.
|
||||
|
||||
if macaroon is None and encrypted_macaroon is None:
|
||||
raise ValueError("Either macaroon or encrypted_macaroon must be provided.")
|
||||
|
||||
if encrypted_macaroon:
|
||||
# if the macaroon is encrypted, decrypt it and return the hex version
|
||||
key = getpass("Enter the macaroon decryption key: ")
|
||||
aes = AESCipher(key.encode())
|
||||
return aes.decrypt(encrypted_macaroon)
|
||||
|
||||
assert macaroon, "macaroon must be set here"
|
||||
:param macaroon: Macaroon encoded in base64 or file path.
|
||||
:type macaroon: str
|
||||
:return: Hex version of macaroon.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
# if the macaroon is a file path, load it and return hex version
|
||||
if macaroon.split(".")[-1] == "macaroon":
|
||||
with open(macaroon, "rb") as f:
|
||||
macaroon_bytes = f.read()
|
||||
return macaroon_bytes.hex()
|
||||
|
||||
# if macaroon is a provided string check if it is hex, if so, return
|
||||
try:
|
||||
bytes.fromhex(macaroon)
|
||||
return macaroon
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# convert the base64 macaroon to hex
|
||||
try:
|
||||
macaroon = base64.b64decode(macaroon).hex()
|
||||
return macaroon
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
else:
|
||||
# if macaroon is a provided string
|
||||
# check if it is hex, if so, return
|
||||
try:
|
||||
bytes.fromhex(macaroon)
|
||||
return macaroon
|
||||
except ValueError:
|
||||
pass
|
||||
# convert the bas64 macaroon to hex
|
||||
try:
|
||||
macaroon = base64.b64decode(macaroon).hex()
|
||||
except Exception:
|
||||
pass
|
||||
return macaroon
|
||||
|
||||
|
||||
# todo: move to its own (crypto.py) file
|
||||
# if this file is executed directly, ask for a macaroon and encrypt it
|
||||
if __name__ == "__main__":
|
||||
macaroon = input("Enter macaroon: ")
|
||||
macaroon = load_macaroon(macaroon)
|
||||
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
|
||||
logger.info("Encrypted macaroon:")
|
||||
logger.info(macaroon)
|
||||
|
||||
+13
-22
@@ -132,7 +132,7 @@ class NWCWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
desc = ""
|
||||
desc_hash = None
|
||||
@@ -148,8 +148,10 @@ class NWCWallet(Wallet):
|
||||
info = await self.conn.get_info()
|
||||
if "make_invoice" not in info["supported_methods"]:
|
||||
return InvoiceResponse(
|
||||
ok=False,
|
||||
error_message="make_invoice is not supported by this NWC service.",
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
"make_invoice is not supported by this NWC service.",
|
||||
)
|
||||
resp = await self.conn.call(
|
||||
"make_invoice",
|
||||
@@ -173,9 +175,7 @@ class NWCWallet(Wallet):
|
||||
"expired": False,
|
||||
}
|
||||
)
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except Exception as e:
|
||||
return InvoiceResponse(ok=False, error_message=str(e))
|
||||
|
||||
@@ -203,9 +203,7 @@ class NWCWallet(Wallet):
|
||||
|
||||
if "lookup_invoice" not in info["supported_methods"]:
|
||||
# if not supported, we assume it succeeded
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=payment_hash, preimage=preimage, fee_msat=0
|
||||
)
|
||||
return PaymentResponse(True, payment_hash, None, preimage, None)
|
||||
|
||||
try:
|
||||
payment_data = await self.conn.call(
|
||||
@@ -215,20 +213,15 @@ class NWCWallet(Wallet):
|
||||
"preimage", None
|
||||
)
|
||||
if not settled:
|
||||
return PaymentResponse(checking_id=payment_hash)
|
||||
return PaymentResponse(None, payment_hash, None, None, None)
|
||||
else:
|
||||
fee_msat = payment_data.get("fees_paid", None)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=payment_hash,
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
)
|
||||
return PaymentResponse(True, payment_hash, fee_msat, preimage, None)
|
||||
except Exception:
|
||||
# Workaround: some nwc service providers might not store the invoice
|
||||
# right away, so this call may raise an exception.
|
||||
# We will assume the payment is pending anyway
|
||||
return PaymentResponse(checking_id=payment_hash)
|
||||
return PaymentResponse(None, payment_hash, None, None, None)
|
||||
except NWCError as e:
|
||||
logger.error("Error paying invoice: " + str(e))
|
||||
failure_codes = [
|
||||
@@ -244,14 +237,13 @@ class NWCWallet(Wallet):
|
||||
]
|
||||
failed = e.code in failure_codes
|
||||
return PaymentResponse(
|
||||
ok=None if not failed else False,
|
||||
None if not failed else False,
|
||||
error_message=e.message if failed else None,
|
||||
)
|
||||
except Exception as e:
|
||||
msg = "Error paying invoice: " + str(e)
|
||||
logger.error(msg)
|
||||
logger.error("Error paying invoice: " + str(e))
|
||||
# assume pending
|
||||
return PaymentResponse(error_message=msg)
|
||||
return PaymentResponse(None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
return await self.get_payment_status(checking_id)
|
||||
@@ -308,7 +300,6 @@ class NWCConnection:
|
||||
self.account_private_key = secp256k1.PrivateKey(bytes.fromhex(secret))
|
||||
self.account_private_key_hex = secret
|
||||
self.account_public_key = self.account_private_key.pubkey
|
||||
assert self.account_public_key
|
||||
self.account_public_key_hex = self.account_public_key.serialize().hex()[2:]
|
||||
|
||||
# Extract service key (used for encryption to identify the nwc service provider)
|
||||
|
||||
@@ -72,7 +72,7 @@ class OpenNodeWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
if description_hash or unhashed_description:
|
||||
raise UnsupportedError("description_hash")
|
||||
@@ -88,15 +88,13 @@ class OpenNodeWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
payment_request = data["lightning_invoice"]["payreq"]
|
||||
self.pending_invoices.append(checking_id)
|
||||
return InvoiceResponse(
|
||||
ok=True, checking_id=checking_id, payment_request=payment_request
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
r = await self.client.post(
|
||||
@@ -107,15 +105,16 @@ class OpenNodeWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"]
|
||||
fee_msat = -data["fee"] * 1000
|
||||
# pending
|
||||
|
||||
if data["status"] != "paid":
|
||||
return PaymentResponse(ok=None, checking_id=checking_id, fee_msat=fee_msat)
|
||||
return PaymentResponse(ok=True, checking_id=checking_id, fee_msat=fee_msat)
|
||||
return PaymentResponse(None, checking_id, fee_msat, None, "payment failed")
|
||||
|
||||
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||
|
||||
+12
-22
@@ -133,31 +133,25 @@ class PhoenixdWallet(Wallet):
|
||||
if r.is_error or "paymentHash" not in data:
|
||||
error_message = data["message"]
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Server error: '{error_message}'"
|
||||
False, None, None, f"Server error: '{error_message}'"
|
||||
)
|
||||
|
||||
checking_id = data["paymentHash"]
|
||||
payment_request = data["serialized"]
|
||||
preimage = data.get("paymentPreimage", None) # if available
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
except json.JSONDecodeError:
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'invalid json response'"
|
||||
False, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message="Server error: 'missing required fields'"
|
||||
False, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(exc)
|
||||
return InvoiceResponse(
|
||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||
False, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
@@ -174,35 +168,31 @@ class PhoenixdWallet(Wallet):
|
||||
data = r.json()
|
||||
|
||||
if "routingFeeSat" not in data and "reason" in data:
|
||||
return PaymentResponse(error_message=data["reason"])
|
||||
return PaymentResponse(None, None, None, None, data["reason"])
|
||||
|
||||
if r.is_error or "paymentHash" not in data:
|
||||
error_message = data["message"] if "message" in data else r.text
|
||||
return PaymentResponse(error_message=error_message)
|
||||
return PaymentResponse(None, None, None, None, error_message)
|
||||
|
||||
checking_id = data["paymentHash"]
|
||||
fee_msat = -int(data["routingFeeSat"]) * 1000
|
||||
preimage = data["paymentPreimage"]
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
)
|
||||
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'invalid json response'"
|
||||
None, None, None, None, "Server error: 'invalid json response'"
|
||||
)
|
||||
except KeyError:
|
||||
return PaymentResponse(
|
||||
error_message="Server error: 'missing required fields'"
|
||||
None, None, None, None, "Server error: 'missing required fields'"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.info(f"Failed to pay invoice {bolt11}")
|
||||
logger.warning(exc)
|
||||
return PaymentResponse(
|
||||
error_message=f"Unable to connect to {self.endpoint}."
|
||||
None, None, None, None, f"Unable to connect to {self.endpoint}."
|
||||
)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
|
||||
+13
-22
@@ -115,6 +115,8 @@ class SparkWallet(Wallet):
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
label = f"lbs{random.random()}"
|
||||
checking_id = label
|
||||
|
||||
try:
|
||||
if description_hash:
|
||||
r = await self.invoicewithdescriptionhash(
|
||||
@@ -136,14 +138,11 @@ class SparkWallet(Wallet):
|
||||
exposeprivatechannels=True,
|
||||
expiry=kwargs.get("expiry"),
|
||||
)
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
payment_request=r["bolt11"],
|
||||
checking_id=label,
|
||||
preimage=r.get("preimage"),
|
||||
)
|
||||
ok, payment_request, error_message = True, r["bolt11"], ""
|
||||
except (SparkError, UnknownError) as e:
|
||||
return InvoiceResponse(ok=False, error_message=str(e))
|
||||
ok, payment_request, error_message = False, None, str(e)
|
||||
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
try:
|
||||
@@ -153,22 +152,17 @@ class SparkWallet(Wallet):
|
||||
)
|
||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||
preimage = r["payment_preimage"]
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=r["payment_hash"],
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
)
|
||||
return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None)
|
||||
|
||||
except (SparkError, UnknownError) as exc:
|
||||
listpays = await self.listpays(bolt11)
|
||||
if not listpays:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
pays = listpays["pays"]
|
||||
|
||||
if len(pays) == 0:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
pay = pays[0]
|
||||
payment_hash = pay["payment_hash"]
|
||||
@@ -180,10 +174,10 @@ class SparkWallet(Wallet):
|
||||
) from exc
|
||||
|
||||
if pay["status"] == "failed":
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
if pay["status"] == "pending":
|
||||
return PaymentResponse(ok=None, checking_id=payment_hash)
|
||||
return PaymentResponse(None, payment_hash, None, None, None)
|
||||
|
||||
if pay["status"] == "complete":
|
||||
r = pay
|
||||
@@ -196,13 +190,10 @@ class SparkWallet(Wallet):
|
||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||
preimage = r["payment_preimage"]
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=r["payment_hash"],
|
||||
fee_msat=fee_msat,
|
||||
preimage=preimage,
|
||||
True, r["payment_hash"], fee_msat, preimage, None
|
||||
)
|
||||
else:
|
||||
return PaymentResponse(ok=False, error_message=str(exc))
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
|
||||
+13
-19
@@ -3,9 +3,9 @@ import hashlib
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .base import (
|
||||
@@ -61,7 +61,7 @@ class ZBDWallet(Wallet):
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**_,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
# https://api.zebedee.io/v0/charges
|
||||
|
||||
@@ -89,25 +89,21 @@ class ZBDWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return InvoiceResponse(ok=False, error_message=error_message)
|
||||
return InvoiceResponse(False, None, None, error_message)
|
||||
|
||||
data = r.json()["data"]
|
||||
checking_id = data["id"] # this is a zbd id
|
||||
payment_request = data["invoice"]["request"]
|
||||
preimage = data["invoice"].get("preimage")
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=checking_id,
|
||||
payment_request=payment_request,
|
||||
preimage=preimage,
|
||||
)
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async def pay_invoice(
|
||||
self, bolt11_invoice: str, fee_limit_msat: int
|
||||
) -> PaymentResponse:
|
||||
# https://api.zebedee.io/v0/payments
|
||||
r = await self.client.post(
|
||||
"payments",
|
||||
json={
|
||||
"invoice": bolt11,
|
||||
"invoice": bolt11_invoice,
|
||||
"description": "",
|
||||
"amount": "",
|
||||
"internalId": "",
|
||||
@@ -118,17 +114,15 @@ class ZBDWallet(Wallet):
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
return PaymentResponse(ok=False, error_message=error_message)
|
||||
return PaymentResponse(False, None, None, None, error_message)
|
||||
|
||||
data = r.json()
|
||||
|
||||
checking_id = bolt11_decode(bolt11).payment_hash
|
||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
||||
fee_msat = -int(data["data"]["fee"])
|
||||
preimage = data["data"]["preimage"]
|
||||
|
||||
return PaymentResponse(
|
||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||
)
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"charges/{checking_id}")
|
||||
@@ -143,7 +137,7 @@ class ZBDWallet(Wallet):
|
||||
"expired": False,
|
||||
"completed": True,
|
||||
}
|
||||
return PaymentStatus(paid=statuses[data.get("status")])
|
||||
return PaymentStatus(statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
r = await self.client.get(f"payments/{checking_id}")
|
||||
@@ -161,7 +155,7 @@ class ZBDWallet(Wallet):
|
||||
"failed": False,
|
||||
}
|
||||
|
||||
return PaymentStatus(paid=statuses[data.get("status")])
|
||||
return PaymentStatus(statuses[data.get("status")], fee_msat=None, preimage=None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
self.queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.12.0"
|
||||
}
|
||||
}
|
||||
Generated
+1613
-7
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -10,9 +10,11 @@
|
||||
"vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css",
|
||||
"vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js",
|
||||
"vendor_minify_components": "./node_modules/.bin/minify ./lnbits/static/bundle-components.js > ./lnbits/static/bundle-components.min.js",
|
||||
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components"
|
||||
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components",
|
||||
"generate": "openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./generated"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "^2.18.4",
|
||||
"concat": "^1.0.3",
|
||||
"minify": "^9.2.0",
|
||||
"prettier": "^3.3.3",
|
||||
@@ -23,10 +25,10 @@
|
||||
"axios": "^1.8.2",
|
||||
"chart.js": "^4.4.4",
|
||||
"moment": "^2.30.1",
|
||||
"nostr-tools": "^2.7.2",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"quasar": "2.17.0",
|
||||
"showdown": "^2.1.0",
|
||||
"nostr-tools": "^2.7.2",
|
||||
"underscore": "^1.13.7",
|
||||
"vue": "3.5.8",
|
||||
"vue-i18n": "^10.0.6",
|
||||
|
||||
Generated
+15
-36
@@ -1303,13 +1303,13 @@ setuptools = "*"
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1327,24 +1327,24 @@ cryptography = ">=2.5"
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
version = "1.0.5"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.16"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<1.0)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
@@ -1688,16 +1688,6 @@ files = [
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
@@ -2471,7 +2461,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
@@ -2479,16 +2468,8 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
@@ -2505,7 +2486,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
@@ -2513,7 +2493,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
@@ -3090,13 +3069,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.2"
|
||||
version = "0.30.6"
|
||||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"},
|
||||
{file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"},
|
||||
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
|
||||
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3105,7 +3084,7 @@ h11 = ">=0.8"
|
||||
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
@@ -3435,4 +3414,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||
content-hash = "602ca9d1ec774a31f3a829faba787742eee7d47c8f27cb2aeab992bd063cd185"
|
||||
content-hash = "f56154a228bfd11ca92c1818dd2b7d71ff67b218225103f4b701e6523ba499ed"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
readme = "README.md"
|
||||
@@ -25,7 +25,7 @@ pyqrcode = "1.2.1"
|
||||
shortuuid = "1.0.13"
|
||||
sse-starlette = "1.8.2"
|
||||
typing-extensions = "4.12.2"
|
||||
uvicorn = "0.34.2"
|
||||
uvicorn = "0.30.6"
|
||||
sqlalchemy = "1.4.54"
|
||||
aiosqlite = "0.20.0"
|
||||
asyncpg = "0.30.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ from lnbits.settings import Settings
|
||||
@pytest.mark.anyio
|
||||
async def test_admin_get_settings_permission_denied(client, from_user):
|
||||
response = await client.get(f"/admin/api/v1/settings?usr={from_user.id}")
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -102,7 +102,7 @@ async def test_login_usr_not_allowed_for_admin_without_credentials(
|
||||
# Attempt to login with user ID for admin
|
||||
response = await http_client.post("/api/v1/auth/usr", json=login_data.dict())
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == 401
|
||||
assert (
|
||||
response.json().get("detail") == "Admin users cannot login with user id only."
|
||||
)
|
||||
@@ -135,7 +135,7 @@ async def test_login_usr_not_allowed(
|
||||
|
||||
response = await http_client.post("/api/v1/auth/usr", json={"usr": user_alan.id})
|
||||
|
||||
assert response.status_code == 403, "Login method not allowed."
|
||||
assert response.status_code == 401, "Login method not allowed."
|
||||
assert response.json().get("detail") == "Login by 'User ID' not allowed."
|
||||
|
||||
settings.auth_allowed_methods = AuthMethods.all()
|
||||
@@ -217,7 +217,7 @@ async def test_login_username_password_not_allowed(
|
||||
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
|
||||
)
|
||||
|
||||
assert response.status_code == 403, "Login method not allowed."
|
||||
assert response.status_code == 401, "Login method not allowed."
|
||||
assert (
|
||||
response.json().get("detail") == "Login by 'Username and Password' not allowed."
|
||||
)
|
||||
@@ -597,7 +597,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
||||
json={},
|
||||
)
|
||||
|
||||
assert response.status_code == 403, "User not authenticated."
|
||||
assert response.status_code == 401, "User not authenticated."
|
||||
assert response.json().get("detail") == "Login with Nostr Auth not allowed."
|
||||
|
||||
settings.auth_allowed_methods = AuthMethods.all()
|
||||
@@ -607,7 +607,7 @@ async def test_register_nostr_not_allowed(http_client: AsyncClient, settings: Se
|
||||
async def test_register_nostr_bad_header(http_client: AsyncClient):
|
||||
response = await http_client.post("/api/v1/auth/nostr")
|
||||
|
||||
assert response.status_code == 400, "Missing header."
|
||||
assert response.status_code == 401, "Missing header."
|
||||
assert response.json().get("detail") == "Nostr Auth header missing."
|
||||
|
||||
response = await http_client.post(
|
||||
@@ -615,7 +615,7 @@ async def test_register_nostr_bad_header(http_client: AsyncClient):
|
||||
headers={"Authorization": "Bearer xyz"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400, "Non nostr header."
|
||||
assert response.status_code == 401, "Non nostr header."
|
||||
assert response.json().get("detail") == "Invalid Authorization scheme."
|
||||
|
||||
response = await http_client.post(
|
||||
@@ -1028,7 +1028,7 @@ async def test_reset_username_password_not_allowed(
|
||||
)
|
||||
settings.auth_allowed_methods = AuthMethods.all()
|
||||
|
||||
assert response.status_code == 403, "Login method not allowed."
|
||||
assert response.status_code == 401, "Login method not allowed."
|
||||
assert (
|
||||
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
||||
)
|
||||
|
||||
@@ -5,12 +5,8 @@ import pytest
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.crud import get_standalone_payment, update_payment
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallet
|
||||
from lnbits.core.models import CreateInvoice, Payment, PaymentState
|
||||
from lnbits.core.services import fee_reserve_total, get_balance_delta
|
||||
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
|
||||
from lnbits.core.services.users import create_user_account
|
||||
from lnbits.exceptions import PaymentError
|
||||
from lnbits.tasks import create_task, wait_for_paid_invoices
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
@@ -105,8 +101,6 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_
|
||||
balance = await get_node_balance_sats()
|
||||
assert balance - prev_balance == create_invoice.amount
|
||||
|
||||
assert payment_status.get("preimage") is not None
|
||||
|
||||
# exit out of infinite loop
|
||||
raise FakeError()
|
||||
|
||||
@@ -157,39 +151,6 @@ async def test_pay_real_invoice_set_pending_and_check_state(
|
||||
assert payment.success
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_pay_real_invoices_in_parallel():
|
||||
user = await create_user_account()
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
|
||||
# more to cover routing feems
|
||||
await update_wallet_balance(wallet, 1100)
|
||||
|
||||
# these must be external invoices
|
||||
real_invoice_one = get_real_invoice(1000)
|
||||
real_invoice_two = get_real_invoice(1000)
|
||||
|
||||
async def pay_first():
|
||||
return await pay_invoice(
|
||||
wallet_id=wallet.id,
|
||||
payment_request=real_invoice_one["payment_request"],
|
||||
)
|
||||
|
||||
async def pay_second():
|
||||
return await pay_invoice(
|
||||
wallet_id=wallet.id,
|
||||
payment_request=real_invoice_two["payment_request"],
|
||||
)
|
||||
|
||||
with pytest.raises(PaymentError, match="Insufficient balance."):
|
||||
await asyncio.gather(pay_first(), pay_second())
|
||||
|
||||
wallet_after = await get_wallet(wallet.id)
|
||||
assert wallet_after
|
||||
assert 0 <= wallet_after.balance <= 100, "One payment should be deducted."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_pay_hold_invoice_check_pending(
|
||||
|
||||
@@ -17,8 +17,6 @@ async def test_create_invoice(from_wallet):
|
||||
amount=1000,
|
||||
memo=description,
|
||||
)
|
||||
assert payment.preimage
|
||||
|
||||
invoice = decode(payment.bolt11)
|
||||
assert invoice.payment_hash == payment.payment_hash
|
||||
assert invoice.amount_msat == 1000000
|
||||
@@ -35,8 +33,6 @@ async def test_create_internal_invoice(from_wallet):
|
||||
payment = await create_invoice(
|
||||
wallet_id=from_wallet.id, amount=1000, memo=description, internal=True
|
||||
)
|
||||
assert payment.preimage
|
||||
|
||||
invoice = decode(payment.bolt11)
|
||||
assert invoice.payment_hash == payment.payment_hash
|
||||
assert invoice.amount_msat == 1000000
|
||||
|
||||
@@ -19,7 +19,6 @@ async def test_services_pay_invoice(to_wallet, real_invoice):
|
||||
assert payment
|
||||
assert payment.status == PaymentState.SUCCESS
|
||||
assert payment.memo == description
|
||||
assert payment.preimage
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from lnbits.core.crud import (
|
||||
create_wallet,
|
||||
get_payments,
|
||||
get_payments_paginated,
|
||||
update_payment,
|
||||
)
|
||||
from lnbits.core.models import PaymentFilters, PaymentState
|
||||
from lnbits.core.services import (
|
||||
create_invoice,
|
||||
create_user_account,
|
||||
update_wallet_balance,
|
||||
)
|
||||
from lnbits.db import Filters
|
||||
from lnbits.core.crud import create_wallet, get_payments, update_payment
|
||||
from lnbits.core.models import PaymentState
|
||||
from lnbits.core.services import create_user_account, update_wallet_balance
|
||||
|
||||
|
||||
async def update_payments(payments):
|
||||
@@ -74,62 +64,3 @@ async def test_crud_get_payments(app):
|
||||
# both false should return failed payments
|
||||
# payments = await get_payments(wallet_id=wallet.id, complete=False, pending=False)
|
||||
# assert len(payments) == 2, "should return 2 failed payment"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_crud_search_payments():
|
||||
|
||||
user = await create_user_account()
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
filters: Filters = Filters(
|
||||
search="",
|
||||
model=PaymentFilters,
|
||||
)
|
||||
# no memo
|
||||
await create_invoice(wallet_id=wallet.id, amount=30, memo="")
|
||||
await create_invoice(wallet_id=wallet.id, amount=30, memo="Invoice A")
|
||||
filters.search = "Invoice A"
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 1, "should return only Invoice A"
|
||||
|
||||
filters.search = "Invoice B"
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 0, "no Invoice B yet"
|
||||
|
||||
for i in range(15):
|
||||
await create_invoice(wallet_id=wallet.id, amount=30 + i, memo="Invoice A")
|
||||
await create_invoice(wallet_id=wallet.id, amount=30 + i, memo="Invoice B")
|
||||
|
||||
filters.search = None
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 32, "should return all payments"
|
||||
|
||||
filters.search = "Invoice A"
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 16
|
||||
|
||||
filters.search = "Invoice B"
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 15
|
||||
|
||||
filters.search = "Invoice"
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
assert page.total == 31
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[
|
||||
"normal_string",
|
||||
b"normal_bytes",
|
||||
b"hex_string".hex(),
|
||||
],
|
||||
)
|
||||
async def test_aes_encrypt_decrypt(key):
|
||||
aes = AESCipher(key)
|
||||
original_text = "Hello, World!"
|
||||
encrypted_text = aes.encrypt(original_text.encode())
|
||||
decrypted_text = aes.decrypt(encrypted_text)
|
||||
assert original_text == decrypted_text
|
||||
@@ -76,7 +76,7 @@ async def test_check_user_exists_with_user_not_allowed(user_alan: User):
|
||||
settings.lnbits_allowed_users = ["only_this_user_id"]
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
|
||||
assert exc_info.value.status_code == 403
|
||||
assert exc_info.value.status_code == 401
|
||||
assert exc_info.value.detail == "User not allowed."
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@ from bolt11 import encode as bolt11_encode
|
||||
from bolt11.types import MilliSatoshi
|
||||
from pytest_mock.plugin import MockerFixture
|
||||
|
||||
from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
|
||||
from lnbits.core.crud.payments import get_payment, get_payments_paginated
|
||||
from lnbits.core.crud import get_standalone_payment, get_wallet
|
||||
from lnbits.core.models import Payment, PaymentState, Wallet
|
||||
from lnbits.core.services import create_invoice, create_user_account, pay_invoice
|
||||
from lnbits.core.services.payments import update_wallet_balance
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
from lnbits.settings import Settings
|
||||
from lnbits.tasks import (
|
||||
@@ -115,62 +113,6 @@ async def test_pay_twice(to_wallet: Wallet):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pay_twice_fast():
|
||||
user = await create_user_account()
|
||||
wallet_one = await create_wallet(user_id=user.id)
|
||||
wallet_two = await create_wallet(user_id=user.id)
|
||||
|
||||
await update_wallet_balance(wallet_one, 1000)
|
||||
payment_a = await create_invoice(wallet_id=wallet_two.id, amount=1000, memo="AAA")
|
||||
payment_b = await create_invoice(wallet_id=wallet_two.id, amount=1000, memo="BBB")
|
||||
|
||||
async def pay_first():
|
||||
return await pay_invoice(
|
||||
wallet_id=wallet_one.id,
|
||||
payment_request=payment_a.bolt11,
|
||||
)
|
||||
|
||||
async def pay_second():
|
||||
return await pay_invoice(
|
||||
wallet_id=wallet_one.id,
|
||||
payment_request=payment_b.bolt11,
|
||||
)
|
||||
|
||||
with pytest.raises(PaymentError, match="Insufficient balance."):
|
||||
await asyncio.gather(pay_first(), pay_second())
|
||||
|
||||
wallet_one_after = await get_wallet(wallet_one.id)
|
||||
assert wallet_one_after
|
||||
assert wallet_one_after.balance == 0, "One payment should be deducted."
|
||||
|
||||
wallet_two_after = await get_wallet(wallet_two.id)
|
||||
assert wallet_two_after
|
||||
assert wallet_two_after.balance == 1000, "One payment received."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_pay_twice_fast_same_invoice(to_wallet: Wallet):
|
||||
payment = await create_invoice(
|
||||
wallet_id=to_wallet.id, amount=3, memo="Twice fast same invoice"
|
||||
)
|
||||
|
||||
async def pay_first():
|
||||
return await pay_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
payment_request=payment.bolt11,
|
||||
)
|
||||
|
||||
async def pay_second():
|
||||
return await pay_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
payment_request=payment.bolt11,
|
||||
)
|
||||
|
||||
with pytest.raises(PaymentError, match="Payment already paid."):
|
||||
await asyncio.gather(pay_first(), pay_second())
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_fake_wallet_pay_external(
|
||||
to_wallet: Wallet, external_funding_source: FakeWallet
|
||||
@@ -237,12 +179,7 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
|
||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, test_name)
|
||||
|
||||
payment = await create_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
amount=123,
|
||||
memo=test_name,
|
||||
webhook="http://test.404.lnbits.com",
|
||||
)
|
||||
payment = await create_invoice(wallet_id=to_wallet.id, amount=123, memo=test_name)
|
||||
await pay_invoice(
|
||||
wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"}
|
||||
)
|
||||
@@ -255,9 +192,6 @@ async def test_notification_for_internal_payment(to_wallet: Wallet):
|
||||
assert _payment.status == PaymentState.SUCCESS.value
|
||||
assert _payment.bolt11 == payment.bolt11
|
||||
assert _payment.amount == 123_000
|
||||
updated_payment = await get_payment(_payment.checking_id)
|
||||
assert updated_payment.webhook_status == "404"
|
||||
|
||||
break # we found our payment, success
|
||||
|
||||
|
||||
@@ -653,55 +587,3 @@ async def test_service_fee(
|
||||
assert service_fee_payment.amount == 422_400
|
||||
assert service_fee_payment.bolt11 == external_invoice.payment_request
|
||||
assert service_fee_payment.preimage is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_payments_for_user(to_wallet: Wallet):
|
||||
all_payments = await get_payments_paginated()
|
||||
total_before = all_payments.total
|
||||
|
||||
user = await create_user_account()
|
||||
wallet_one = await create_wallet(user_id=user.id, wallet_name="first wallet")
|
||||
wallet_two = await create_wallet(user_id=user.id, wallet_name="second wallet")
|
||||
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 0
|
||||
|
||||
payment = await create_invoice(wallet_id=wallet_one.id, amount=100, memo="one")
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 1
|
||||
# this will create a payment in the to_wallet that we need to count for at the end
|
||||
await pay_invoice(
|
||||
wallet_id=to_wallet.id,
|
||||
payment_request=payment.bolt11,
|
||||
)
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 1
|
||||
|
||||
payment = await create_invoice(wallet_id=wallet_one.id, amount=3, memo="two")
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 2
|
||||
|
||||
payment = await create_invoice(wallet_id=wallet_two.id, amount=3, memo="three")
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 3
|
||||
|
||||
await pay_invoice(
|
||||
wallet_id=wallet_one.id,
|
||||
payment_request=payment.bolt11,
|
||||
)
|
||||
user_payments = await get_payments_paginated(user_id=user.id)
|
||||
assert user_payments.total == 4
|
||||
|
||||
all_payments = await get_payments_paginated()
|
||||
total_after = all_payments.total
|
||||
|
||||
assert total_after == total_before + 5, "Total payments should be updated."
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_payments_for_non_user():
|
||||
user_payments = await get_payments_paginated(user_id="nonexistent")
|
||||
assert (
|
||||
user_payments.total == 0
|
||||
), "No payments should be found for non-existent user."
|
||||
|
||||
@@ -326,12 +326,11 @@
|
||||
"call_params": {
|
||||
"amount": 555,
|
||||
"memo": "Test Invoice",
|
||||
"label": "test-label",
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"label": "test-label"
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n",
|
||||
"error_message": null
|
||||
},
|
||||
@@ -356,7 +355,7 @@
|
||||
"response_type": "data",
|
||||
"response": {
|
||||
"ln_invoice": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"bolt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
|
||||
}
|
||||
}
|
||||
@@ -379,13 +378,12 @@
|
||||
"expiry": null,
|
||||
"exposeprivatechannels": true,
|
||||
"label": "test-label",
|
||||
"amount_msat": 555000,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"amount_msat": 555000
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
"response": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"bolt11": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
|
||||
}
|
||||
}
|
||||
@@ -404,14 +402,12 @@
|
||||
"kwargs": {
|
||||
"value": 555,
|
||||
"private": true,
|
||||
"memo": "Test Invoice",
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")",
|
||||
"__eval__:r_preimage": "bytes.fromhex(\"0000000000000000000000000000000000000000000000000000000000000001\")"
|
||||
"memo": "Test Invoice"
|
||||
}
|
||||
},
|
||||
"response_type": "data",
|
||||
"response": {
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")",
|
||||
"__eval__:r_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")",
|
||||
"payment_request": "lnbc5550n1pnq9jg3sp52rvwstvjcypjsaenzdh0h30jazvzsf8aaye0julprtth9kysxtuspp5e5s3z7felv4t9zrcc6wpn7ehvjl5yzewanzl5crljdl3jgeffyhqdq2f38xy6t5wvxqzjccqpjrzjq0yzeq76ney45hmjlnlpvu0nakzy2g35hqh0dujq8ujdpr2e42pf2rrs6vqpgcsqqqqqqqqqqqqqqeqqyg9qxpqysgqwftcx89k5pp28435pgxfl2vx3ksemzxccppw2j9yjn0ngr6ed7wj8ztc0d5kmt2mvzdlcgrludhz7jncd5l5l9w820hc4clpwhtqj3gq62g66n"
|
||||
}
|
||||
}
|
||||
@@ -426,8 +422,7 @@
|
||||
"call_params": {
|
||||
"amount": 555,
|
||||
"memo": "Test Invoice",
|
||||
"label": "test-label",
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"label": "test-label"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -475,8 +470,7 @@
|
||||
"expiry": null,
|
||||
"exposeprivatechannels": true,
|
||||
"label": "test-label",
|
||||
"amount_msat": 555000,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"amount_msat": 555000
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -497,8 +491,7 @@
|
||||
"expiry": null,
|
||||
"exposeprivatechannels": true,
|
||||
"label": "test-label",
|
||||
"amount_msat": 555000,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"amount_msat": 555000
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -522,9 +515,7 @@
|
||||
"kwargs": {
|
||||
"value": 555,
|
||||
"private": true,
|
||||
"memo": "Test Invoice",
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")",
|
||||
"__eval__:r_preimage": "bytes.fromhex(\"0000000000000000000000000000000000000000000000000000000000000001\")"
|
||||
"memo": "Test Invoice"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -543,8 +534,7 @@
|
||||
"call_params": {
|
||||
"amount": 555,
|
||||
"memo": "Test Invoice",
|
||||
"label": "test-label",
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"label": "test-label"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -569,8 +559,7 @@
|
||||
"expiry": null,
|
||||
"exposeprivatechannels": true,
|
||||
"label": "test-label",
|
||||
"amount_msat": 555000,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"amount_msat": 555000
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -587,8 +576,7 @@
|
||||
"call_params": {
|
||||
"amount": 555,
|
||||
"memo": "Test Invoice",
|
||||
"label": "test-label",
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"label": "test-label"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -613,8 +601,7 @@
|
||||
"expiry": null,
|
||||
"exposeprivatechannels": true,
|
||||
"label": "test-label",
|
||||
"amount_msat": 555000,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
"amount_msat": 555000
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -702,7 +689,7 @@
|
||||
"expect": {
|
||||
"error_message": null,
|
||||
"success": true,
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"fee_msat": 50,
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
@@ -752,7 +739,7 @@
|
||||
"response": {
|
||||
"amount_sent_msat": 21000,
|
||||
"amount_msat": 21050,
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
}
|
||||
@@ -787,7 +774,7 @@
|
||||
}
|
||||
],
|
||||
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,7 +806,7 @@
|
||||
}
|
||||
],
|
||||
"payment_preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1250,7 +1237,7 @@
|
||||
{
|
||||
"description": "success",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
@@ -1289,7 +1276,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1304,7 +1291,7 @@
|
||||
"invoices": [
|
||||
{
|
||||
"status": "paid",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1328,13 +1315,12 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.lightning_pb2.PaymentHash",
|
||||
"kwargs": {
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:r_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "data",
|
||||
"response": {
|
||||
"settled": true,
|
||||
"__eval__:r_preimage": "bytes.fromhex(\"0000000000000000000000000000000000000000000000000000000000000001\")"
|
||||
"settled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1346,7 +1332,7 @@
|
||||
{
|
||||
"description": "pending",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -1378,7 +1364,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1390,7 +1376,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1409,7 +1395,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1421,7 +1407,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -1446,7 +1432,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1458,7 +1444,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -1477,7 +1463,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1489,7 +1475,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1506,7 +1492,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1518,7 +1504,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1542,7 +1528,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1554,7 +1540,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1562,7 +1548,7 @@
|
||||
"invoices": [
|
||||
{
|
||||
"status": "unpaid",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1587,7 +1573,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.lightning_pb2.PaymentHash",
|
||||
"kwargs": {
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:r_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "data",
|
||||
@@ -1605,7 +1591,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.lightning_pb2.PaymentHash",
|
||||
"kwargs": {
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:r_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -1625,7 +1611,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.lightning_pb2.PaymentHash",
|
||||
"kwargs": {
|
||||
"__eval__:r_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:r_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -1705,7 +1691,7 @@
|
||||
"description": "failed",
|
||||
"description1": "pending should be false in the 'expect', this is a bug",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -1745,7 +1731,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1757,7 +1743,7 @@
|
||||
"request_type": "function",
|
||||
"request_data": {
|
||||
"kwargs": {
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
},
|
||||
"response_type": "json",
|
||||
@@ -1765,7 +1751,7 @@
|
||||
"invoices": [
|
||||
{
|
||||
"status": "expired",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1837,7 +1823,7 @@
|
||||
{
|
||||
"description": "success",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
@@ -1886,7 +1872,7 @@
|
||||
"pays": [
|
||||
{
|
||||
"status": "complete",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417",
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
|
||||
"preimage": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"amount_sent_msat": 21000,
|
||||
"amount_msat": 21050
|
||||
@@ -1914,7 +1900,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -1947,7 +1933,7 @@
|
||||
{
|
||||
"description": "success status, no payment found",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
@@ -1974,7 +1960,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -1994,7 +1980,7 @@
|
||||
{
|
||||
"description": "pending",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -2087,7 +2073,7 @@
|
||||
"pays": [
|
||||
{
|
||||
"status": "pending",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2131,7 +2117,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2152,7 +2138,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2168,7 +2154,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2184,7 +2170,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2212,7 +2198,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2240,7 +2226,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
@@ -2268,7 +2254,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "exception",
|
||||
@@ -2330,7 +2316,7 @@
|
||||
"description": "failed",
|
||||
"description1": "pending should be false in the 'expect', this is a bug",
|
||||
"call_params": {
|
||||
"checking_id": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"checking_id": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
},
|
||||
"expect": {
|
||||
"success": false,
|
||||
@@ -2374,7 +2360,7 @@
|
||||
"pays": [
|
||||
{
|
||||
"status": "failed",
|
||||
"payment_hash": "c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417"
|
||||
"payment_hash": "e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2399,7 +2385,7 @@
|
||||
"request_data": {
|
||||
"klass": "lnbits.wallets.lnd_grpc_files.router_pb2.TrackPaymentRequest",
|
||||
"kwargs": {
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"c386d8e8d07342f2e39e189c8e6c57bb205bb373fe4e3a6f69404a8bb767b417\")"
|
||||
"__eval__:payment_hash": "bytes.fromhex(\"e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96\")"
|
||||
}
|
||||
},
|
||||
"response_type": "__aiter__",
|
||||
|
||||
Reference in New Issue
Block a user