Compare commits

..
Author SHA1 Message Date
dni ⚡ c820456500 feat: add openapi-generators for sdk's 2025-04-10 12:29:41 +02:00
72 changed files with 2333 additions and 1326 deletions
+2
View File
@@ -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
View File
@@ -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"
+7 -3
View File
@@ -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
+6 -34
View File
@@ -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
+1 -1
View File
@@ -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
-67
View File
@@ -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()
-2
View File
@@ -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",
+2 -10
View File
@@ -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
+1 -17
View File
@@ -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:
-14
View File
@@ -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()
-11
View File
@@ -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
+1 -1
View File
@@ -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
+11 -13
View File
@@ -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):
+24 -52
View File
@@ -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)}"
+6 -2
View File
@@ -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">
+6 -6
View File
@@ -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"
+2 -2
View File
@@ -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()
+19 -23
View File
@@ -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"))
+3 -11
View File
@@ -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
View File
@@ -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 {}
+4 -4
View File
@@ -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
View File
@@ -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
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -3
View File
@@ -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',
+2 -4
View File
@@ -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',
+3 -3
View File
@@ -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)
})
+3 -5
View File
@@ -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: ''
}
},
+2 -2
View File
@@ -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')
+1 -1
View File
@@ -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: `
+11 -7
View File
@@ -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
}
+1 -1
View File
@@ -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
+10 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]
}
+6 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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:
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+14 -26
View File
@@ -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:
+27 -34
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6 -11
View File
@@ -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:
+32 -32
View File
@@ -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
View File
@@ -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)
+8 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.12.0"
}
}
+1613 -7
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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
+7 -7
View File
@@ -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."
)
-39
View File
@@ -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
+3 -72
View File
@@ -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
-20
View File
@@ -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
+1 -1
View File
@@ -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."
+3 -121
View File
@@ -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."
+62 -76
View File
@@ -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__",