Compare commits

..
Author SHA1 Message Date
Arcanddni ⚡ 88b01e6079 feat: theme fix for elsal, cyber, freedom (#3259) 2025-07-11 12:39:03 +02:00
Arcanddni ⚡ b0d0901969 refactor: theme options, includes fix bg on firefox with dialog (#3247)
Co-authored-by: dni  <office@dnilabs.com>
2025-07-11 12:39:02 +02:00
dni ⚡ 6563610b6c chore: update to version 1.2.1 2025-07-11 12:38:33 +02:00
dni ⚡ c4aa0b598c chore: update to version 1.2.0 2025-07-08 10:22:22 +02:00
dni ⚡ aab76e691d chore: update to version v1.2.0-rc3 (#3231) 2025-07-08 10:17:30 +02:00
Vlad Stananddni ⚡ 3ed30be93e fix: condition for available, but not installed extension (#3232) 2025-07-08 10:17:29 +02:00
Vlad Stananddni ⚡ 37f99e7f96 doc: LNBITS_EXTENSIONS_PATH (#3229) 2025-07-08 10:17:28 +02:00
dni ⚡ a16078a6ba chore: update python packages + formatting + cleanup (#3221) 2025-07-08 10:17:27 +02:00
dni ⚡ c4c03d96a3 test: add regtest testcase for no route failure (#3189) 2025-07-08 10:17:25 +02:00
Vlad Stananddni ⚡ ab3248da15 fix: url_for should not return extension upgrades temp paths (#3227) 2025-07-08 10:17:24 +02:00
dni ⚡ b4b37cd733 fix: circular import on handle_fiat_payments (#3226) 2025-07-08 10:17:23 +02:00
dni ⚡ 26eb7a449c feat: add internal_invoice_queue_put to get rid of dynamic import (#3223) 2025-07-08 10:17:21 +02:00
Vlad Stananddni ⚡ 695e9b6471 [feat] Add stripe payments (#3184) 2025-07-08 10:17:19 +02:00
blackcoffeexbtanddni ⚡ 5c9511ccfe Label tweaks (#3224) 2025-07-08 10:17:18 +02:00
Vlad Stananddni ⚡ c7b7832a88 feat: add external id for users (#3219) 2025-07-08 10:17:16 +02:00
75 changed files with 616 additions and 2022 deletions
+1 -21
View File
@@ -40,7 +40,7 @@ PORT=5000
######################################
# which fundingsources are allowed in the admin ui
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet"
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
@@ -61,20 +61,6 @@ CLICHE_ENDPOINT=ws://127.0.0.1:12000
SPARK_URL=http://localhost:9737/rpc
SPARK_TOKEN=myaccesstoken
#CLNRest (using runes)
CLNREST_URL=https://127.0.0.1:3010
CLNREST_CA=/home/lightningd/.lightning/bitcoin/ca.pem
CLNREST_CERT=/home/lightningd/.lightning/bitcoin/server.pem
# CLNREST_CA = cat ca.pem | awk '{printf "%s\\n", $0} END {printf "\n"}'
# CLNREST_CERT = cat server.pem | awk '{printf "%s\\n", $0} END {printf "\n"}'
CLNREST_READONLY_RUNE=lightning-cli createrune restrictions='[["method=listfunds", "method=listpays", "method=listinvoices", "method=getinfo", "method=summary", "method=waitanyinvoice"]]' | jq -r .rune
CLNREST_INVOICE_RUNE=lightning-cli createrune restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' | jq -r .rune
CLNREST_PAY_RUNE=lightning-cli createrune restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune
#CLNREST_RENEPAY_RUNE=lightning-cli createrune restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune
#CLNREST_LAST_PAY_INDEX='lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max'
#CLNREST_NODEID=lightning-cli getinfo | jq -r .id # only required for v23.08
# CoreLightningWallet
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
@@ -167,12 +153,6 @@ BREEZ_GREENLIGHT_DEVICE_KEY="/path/to/breezsdk/device.pem" # or BASE64/HEXSTRIN
BREEZ_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
# BREEZ_USE_TRAMPOLINE=true
# BreezLiquidSdkWallet
# get your own api key here https://breez.technology/request-api-key/#contact-us-form-sdk
# or keep the api key empty to use the LNbits key for referrals (API key is not a secret)
# BREEZ_LIQUID_API_KEY=""
BREEZ_LIQUID_SEED="MNEMONIC SEED PHRASE"
# BREEZ_LIQUID_FEE_OFFSET_SAT=50
# Google OAuth Config
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
+3 -1
View File
@@ -43,7 +43,9 @@ runs:
shell: bash
run: |
poetry env use python${{ inputs.python-version }}
poetry install --all-extras
poetry install
# needed for conv tests
poetry add psycopg2-binary
- name: Use Node.js ${{ inputs.node-version }}
if: ${{ (inputs.npm == 'true') }}
-31
View File
@@ -12,23 +12,6 @@ A backend wallet can be configured using the following LNbits environment variab
You can [compare the LNbits compatible Lightning Network funding sources here](wallets.md).
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
[Core lightning Rest API docs](https://docs.corelightning.org/docs/rest)
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
- `LNBITS_BACKEND_WALLET_CLASS`: **CLNRestWallet**
- `CLNREST_URL`: `https://127.0.0.1:3010`
- `CLNREST_CA`: `/home/lightningd/.lightning/bitcoin/ca.pem` (or the content of the `ca.pem` file)
- `CLNREST_CERT`: `/home/lightningd/.lightning/bitcoin/server.pem` (or the content of the `server.pem` file)
- `CLNREST_READONLY_RUNE`: `lightning-cli createrune restrictions='[["method=listfunds", "method=listpays", "method=listinvoices", "method=getinfo", "method=summary", "method=waitanyinvoice"]]' | jq -r .rune`
- `CLNREST_INVOICE_RUNE`: `lightning-cli createrune restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' | jq -r .rune`
- `CLNREST_PAY_RUNE`: `lightning-cli createrune restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune`
- `CLNREST_RENEPAY_RUNE`: `lightning-cli createrune restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune`
- `CLNREST_LAST_PAY_INDEX`: `lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max' `
- `CLNREST_NODEID`: `lightning-cli getinfo | jq -r .id` (only required for v23.08)
### CoreLightning
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
@@ -36,8 +19,6 @@ Should also work with the [Rust version of CLNRest](https://github.com/daywalker
### CoreLightning REST
This is the old REST interface that uses [Ride The Lightning/c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
@@ -154,18 +135,6 @@ A Greenlight invite code or Greenlight partner certificate/key can be used to re
- `BREEZ_GREENLIGHT_DEVICE_KEY`: /path/to/breezsdk/device.pem or Base64/Hex
- `BREEZ_GREENLIGHT_DEVICE_CERT`: /path/to/breezsdk/device.crt or Base64/Hex
### Breez Liquid SDK
This funding source leverages the [Breez SDK - Liquid](https://sdk-doc-liquid.breez.technology/) to manage all Lightning payments via submarine swaps on the Liquid network. To get started, simply provide a mnemonic seed phrase. The easiest way to generate one is by using a liquid wallet, such as [Blockstream Green](https://blockstream.com/green/). Once generated, you can copy the seed to your environment variable or enter it in the admin UI.
- `LNBITS_BACKEND_WALLET_CLASS`: **BreezLiquidSdkWallet**
- `BREEZ_LIQUID_SEED`: ...
Each submarine swap incurs service and on-chain fees. To account for these, you may need to increase the reserve fee in the admin UI by navigating to **Settings -> Funding**, or by setting the following environment variables:
- `LNBITS_RESERVE_FEE_MIN`: ...
- `LNBITS_RESERVE_FEE_PERCENT`: ...
### Cliche Wallet
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
+9 -6
View File
@@ -27,7 +27,6 @@ from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.services.notifications import enqueue_notification
from lnbits.core.services.payments import check_pending_payments
from lnbits.core.tasks import (
audit_queue,
collect_exchange_rates_data,
@@ -66,7 +65,11 @@ from .middleware import (
add_ip_block_middleware,
add_ratelimit_middleware,
)
from .tasks import internal_invoice_listener, invoice_listener, run_interval
from .tasks import (
check_pending_payments,
internal_invoice_listener,
invoice_listener,
)
async def startup(app: FastAPI):
@@ -274,7 +277,7 @@ async def check_installed_extensions(app: FastAPI):
logger.info(f"{ext.id} ({ext.installed_version})")
async def build_all_installed_extensions_list( # noqa: C901
async def build_all_installed_extensions_list(
include_deactivated: Optional[bool] = True,
) -> list[InstallableExtension]:
"""
@@ -448,7 +451,7 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
app.include_router(router=ext_route, prefix=prefix)
async def check_and_register_extensions(app: FastAPI) -> None:
async def check_and_register_extensions(app: FastAPI):
await check_installed_extensions(app)
for ext in await get_valid_extensions(False):
try:
@@ -458,12 +461,12 @@ async def check_and_register_extensions(app: FastAPI) -> None:
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
def register_async_tasks() -> None:
def register_async_tasks():
create_permanent_task(wait_for_audit_data)
create_permanent_task(wait_notification_messages)
create_permanent_task(run_interval(30 * 60, check_pending_payments))
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
+7 -7
View File
@@ -320,8 +320,9 @@ async def extensions_list():
from lnbits.app import build_all_installed_extensions_list
for ext in await build_all_installed_extensions_list():
if not ext.meta or not ext.meta.installed_release:
raise ValueError(f"Extension {ext.id} has no installed_release")
assert (
ext.meta and ext.meta.installed_release
), f"Extension {ext.id} has no installed_release"
click.echo(f" - {ext.id} ({ext.meta.installed_release.version})")
@@ -352,7 +353,7 @@ async def extensions_list():
help="Admin user ID (must have permissions to install extensions).",
)
@coro
async def extensions_update( # noqa: C901
async def extensions_update(
extension: Optional[str] = None,
all_extensions: Optional[bool] = False,
repo_index: Optional[str] = None,
@@ -607,10 +608,9 @@ async def update_extension(
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
if not installed_ext.meta or not installed_ext.meta.installed_release:
raise ValueError(
"Cannot find previously installed release. Please uninstall first."
)
assert (
installed_ext.meta and installed_ext.meta.installed_release
), "Cannot find previously installed release. Please uninstall first."
release = await _select_release(extension, repo_index, source_repo)
if not release:
+3 -9
View File
@@ -30,11 +30,10 @@ async def delete_expired_audit_entries(
conn: Optional[Connection] = None,
):
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE from audit
WHERE delete_at < {db.timestamp_now}
""", # noqa: S608
""",
)
@@ -49,16 +48,13 @@ async def get_count_stats(
filters = Filters()
clause = filters.where()
data = await (conn or db).fetchall(
# SQL injection vectors safety:
# - `field` is a static string, not user input
# - `clause` is generated from filters, which are validated and sanitized
query=f"""
SELECT {field} as field, count({field}) as total
FROM audit
{clause}
GROUP BY {field}
ORDER BY {field}
""", # noqa: S608
""",
values=filters.values(),
model=AuditCountStat,
)
@@ -74,15 +70,13 @@ async def get_long_duration_stats(
filters = Filters()
clause = filters.where()
long_duration_paths = await (conn or db).fetchall(
# This query is safe from SQL injection
# The `clause` is constructed from sanitized inputs
query=f"""
SELECT path as field, max(duration) as total FROM audit
{clause}
GROUP BY path
ORDER BY total DESC
LIMIT 5
""", # noqa: S608
""",
values=filters.values(),
model=AuditCountStat,
)
+3 -7
View File
@@ -50,8 +50,7 @@ async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> N
{"id": ext_id},
)
# Check that 'ext_id' is a valid extension id and not a malicious string
if not row:
raise Exception(f"Extension '{ext_id}' db version cannot be found")
assert row, f"Extension '{ext_id}' db version cannot be found"
is_file_based_db = await Database.clean_ext_db_files(ext_id)
if is_file_based_db:
@@ -79,13 +78,10 @@ async def get_installed_extensions(
active: Optional[bool] = None,
conn: Optional[Connection] = None,
) -> list[InstallableExtension]:
query = "SELECT * FROM installed_extensions"
if active is not None:
query += " WHERE active = :active"
where = "WHERE active = :active" if active is not None else ""
values = {"active": active} if active is not None else {}
all_extensions = await (conn or db).fetchall(
query,
f"SELECT * FROM installed_extensions {where}",
values,
model=InstallableExtension,
)
+25 -47
View File
@@ -19,13 +19,8 @@ from ..models import (
)
async def update_payment_extra(
payment: Payment, conn: Optional[Connection] = None
) -> Payment:
return await (conn or db).execute(
"UPDATE apipayments SET extra = :extra WHERE checking_id = :checking_id",
{"extra": payment.extra, "checking_id": payment.checking_id},
)
def update_payment_extra():
pass
async def get_payment(checking_id: str, conn: Optional[Connection] = None) -> Payment:
@@ -55,13 +50,11 @@ async def get_standalone_payment(
clause = f"({clause}) AND wallet_id = :wallet_id"
row = await (conn or db).fetchone(
# This query is safe from SQL injection
# The `clause` is constructed from sanitized inputs
f"""
SELECT * FROM apipayments
WHERE {clause}
ORDER BY amount LIMIT 1
""", # noqa: S608
""",
values,
Payment,
)
@@ -87,25 +80,19 @@ async def get_latest_payments_by_extension(
ext_name: str, ext_id: str, limit: int = 5
) -> list[Payment]:
return await db.fetchall(
# This query is safe from SQL injection
# The limtit is an integer and not user input
f"""
SELECT * FROM apipayments
WHERE status = :status
WHERE status = '{PaymentState.SUCCESS}'
AND extra LIKE :ext_name
AND extra LIKE :ext_id
ORDER BY time DESC LIMIT {int(limit)}
""", # noqa: S608
{
"status": f"{PaymentState.SUCCESS}",
"ext_name": f"%{ext_name}%",
"ext_id": f"%{ext_id}%",
},
""",
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
Payment,
)
async def get_payments_paginated( # noqa: C901
async def get_payments_paginated(
*,
wallet_id: Optional[str] = None,
user_id: Optional[str] = None,
@@ -240,23 +227,21 @@ async def delete_expired_invoices(
# first we delete all invoices older than one month
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE FROM apipayments
WHERE status = :status AND amount > 0
WHERE status = '{PaymentState.PENDING}' AND amount > 0
AND time < {db.timestamp_placeholder("delta")}
""", # noqa: S608
{"status": f"{PaymentState.PENDING}", "delta": int(time() - 2592000)},
""",
{"delta": int(time() - 2592000)},
)
# then we delete all invoices whose expiry date is in the past
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE FROM apipayments
WHERE status = :status AND amount > 0
WHERE status = '{PaymentState.PENDING}' AND amount > 0
AND expiry < {db.timestamp_placeholder("now")}
""", # noqa: S608
{"status": f"{PaymentState.PENDING}", "now": int(time())},
""",
{"now": int(time())},
)
@@ -269,8 +254,7 @@ async def create_payment(
# we don't allow the creation of the same invoice twice
# note: this can be removed if the db uniqueness constraints are set appropriately
previous_payment = await get_standalone_payment(checking_id, conn=conn)
if previous_payment is not None:
raise ValueError("Payment already exists")
assert previous_payment is None, "Payment already exists"
extra = data.extra or {}
payment = Payment(
@@ -284,7 +268,7 @@ async def create_payment(
preimage=data.preimage,
expiry=data.expiry,
webhook=data.webhook,
fee=-abs(data.fee),
fee=data.fee,
tag=extra.get("tag", None),
extra=extra,
)
@@ -337,20 +321,16 @@ async def get_payments_history(
)
"""
]
clause = filters.where(where)
transactions: list[dict] = await db.fetchall(
# This query is safe from SQL injection:
# - `date_trunc` is a static string, not user input
# - `clause` is generated from filters, which are validated and sanitized
f"""
SELECT {date_trunc} date,
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) income,
SUM(CASE WHEN amount < 0 THEN abs(amount) + abs(fee) ELSE 0 END) spending
FROM apipayments
{clause}
{filters.where(where)}
GROUP BY date
ORDER BY date DESC
""", # noqa: S608
""",
filters.values(values),
)
if wallet_id:
@@ -385,6 +365,7 @@ async def get_payment_count_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentCountStat]:
if not filters:
filters = Filters()
extra_stmts = []
@@ -395,16 +376,13 @@ async def get_payment_count_stats(
clause = filters.where(extra_stmts)
data = await (conn or db).fetchall(
# SQL injection vectors safety:
# - `field` is a static string, not user input
# - `clause` is generated from filters, which are validated and sanitized
query=f"""
SELECT {field} as field, count(*) as total
FROM apipayments
{clause}
GROUP BY {field}
ORDER BY {field}
""", # noqa: S608
""",
values=filters.values(),
model=PaymentCountStat,
)
@@ -417,6 +395,7 @@ async def get_daily_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
if not filters:
filters = Filters()
@@ -466,6 +445,7 @@ async def get_wallets_stats(
user_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> list[PaymentWalletStats]:
if not filters:
filters = Filters()
@@ -488,8 +468,6 @@ async def get_wallets_stats(
clauses = filters.where(where_stmts)
data = await (conn or db).fetchall(
# This query is safe from SQL injection
# The `clauses` is constructed from sanitized inputs
query=f"""
SELECT apipayments.wallet_id,
MAX(wallets.name) AS wallet_name,
@@ -501,7 +479,7 @@ async def get_wallets_stats(
{clauses}
GROUP BY apipayments.wallet_id
ORDER BY payments_count
""", # noqa: S608
""",
values=filters.values(),
model=PaymentWalletStats,
)
@@ -526,11 +504,11 @@ async def check_internal(
otherwise None
"""
return await (conn or db).fetchone(
"""
f"""
SELECT * FROM apipayments
WHERE payment_hash = :hash AND status = :status AND amount > 0
WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0
""",
{"status": f"{PaymentState.PENDING}", "hash": payment_hash},
{"hash": payment_hash},
Payment,
)
+2 -4
View File
@@ -56,8 +56,7 @@ async def update_admin_settings(
async def update_super_user(super_user: str) -> SuperSettings:
await set_settings_field("super_user", super_user)
settings = await get_super_settings()
if not settings:
raise ValueError("updated super_user settings could not be retrieved")
assert settings, "updated super_user settings could not be retrieved"
return settings
@@ -87,8 +86,7 @@ async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSet
await set_settings_field(key, value)
settings = await get_super_settings()
if not settings:
raise ValueError("created admin settings could not be retrieved")
assert settings, "created admin settings could not be retrieved"
return settings
+1 -2
View File
@@ -110,7 +110,6 @@ async def delete_accounts_no_wallets(
) -> None:
delta = int(time()) - time_delta
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
DELETE FROM accounts
WHERE NOT EXISTS (
@@ -119,7 +118,7 @@ async def delete_accounts_no_wallets(
(updated_at is null AND created_at < :delta)
OR updated_at < {db.timestamp_placeholder("delta")}
)
""", # noqa: S608
""",
{"delta": delta},
)
+21 -26
View File
@@ -48,12 +48,11 @@ async def delete_wallet(
) -> None:
now = int(time())
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE wallets
SET deleted = :deleted, updated_at = {db.timestamp_placeholder('now')}
WHERE id = :wallet AND "user" = :user
""", # noqa: S608
""",
{"wallet": wallet_id, "user": user_id, "deleted": deleted, "now": now},
)
@@ -72,12 +71,11 @@ async def delete_wallet_by_id(
) -> Optional[int]:
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE wallets
SET deleted = true, updated_at = {db.timestamp_placeholder('now')}
WHERE id = :wallet
""", # noqa: S608
""",
{"wallet": wallet_id, "now": now},
)
return result.rowcount
@@ -109,16 +107,14 @@ async def delete_unused_wallets(
async def get_wallet(
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
) -> Optional[Wallet]:
query = """
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
WHERE id = :wallet
"""
if deleted is not None:
query += " AND deleted = :deleted "
where = "AND deleted = :deleted" if deleted is not None else ""
return await (conn or db).fetchone(
query,
f"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
WHERE id = :wallet {where}
""",
{"wallet": wallet_id, "deleted": deleted},
Wallet,
)
@@ -127,16 +123,14 @@ async def get_wallet(
async def get_wallets(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
) -> list[Wallet]:
query = """
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
WHERE "user" = :user
"""
if deleted is not None:
query += " AND deleted = :deleted "
where = "AND deleted = :deleted" if deleted is not None else ""
return await (conn or db).fetchall(
query,
f"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0) AS balance_msat FROM wallets
WHERE "user" = :user {where}
""",
{"user": user_id, "deleted": deleted},
Wallet,
)
@@ -168,11 +162,12 @@ async def get_wallets_paginated(
async def get_wallets_ids(
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
) -> list[str]:
query = """SELECT id FROM wallets WHERE "user" = :user"""
if deleted is not None:
query += "AND deleted = :deleted"
where = "AND deleted = :deleted" if deleted is not None else ""
result: list[dict] = await (conn or db).fetchall(
query,
f"""
SELECT id FROM wallets
WHERE "user" = :user {where}
""",
{"user": user_id, "deleted": deleted},
)
return [row["id"] for row in result]
+1 -2
View File
@@ -37,8 +37,7 @@ async def create_webpush_subscription(
{"endpoint": endpoint, "user": user, "data": data, "host": host},
)
subscription = await get_webpush_subscription(endpoint, user)
if not subscription:
raise ValueError("Newly created webpush subscription couldn't be retrieved")
assert subscription, "Newly created webpush subscription couldn't be retrieved"
return subscription
+8 -20
View File
@@ -214,7 +214,6 @@ async def m007_set_invoice_expiries(db: Connection):
"""
try:
result = await db.execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
SELECT bolt11, checking_id
FROM apipayments
@@ -223,7 +222,7 @@ async def m007_set_invoice_expiries(db: Connection):
AND bolt11 IS NOT NULL
AND expiry IS NULL
AND time < {db.timestamp_now}
""" # noqa: S608
"""
)
rows = result.mappings().all()
if len(rows):
@@ -243,15 +242,13 @@ async def m007_set_invoice_expiries(db: Connection):
f" {invoice.payment_hash} to {expiration_date}"
)
await db.execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE apipayments SET expiry = {db.timestamp_placeholder('expiry')}
WHERE checking_id = :checking_id AND amount > 0
""", # noqa: S608
""",
{"expiry": expiration_date, "checking_id": checking_id},
)
except Exception as exc:
logger.debug(exc)
except Exception:
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
@@ -374,8 +371,7 @@ async def m014_set_deleted_wallets(db: Connection):
"wallet": row.get("id"),
},
)
except Exception as exc:
logger.debug(exc)
except Exception:
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
@@ -458,19 +454,17 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
# set all to now where they are null
now = int(time())
await db.execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE wallets SET created_at = {db.timestamp_placeholder('now')}
WHERE created_at IS NULL
""", # noqa: S608
""",
{"now": now},
)
await db.execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE accounts SET created_at = {db.timestamp_placeholder('now')}
WHERE created_at IS NULL
""", # noqa: S608
""",
{"now": now},
)
@@ -622,12 +616,7 @@ async def m027_update_apipayments_data(db: Connection):
logger.info(f"Updating {offset} to {offset+limit}")
result = await db.execute(
# Limit and Offset safe from SQL injection
# since they are integers and are not user input
f"""
SELECT * FROM apipayments
ORDER BY time LIMIT {int(limit)} OFFSET {int(offset)}
""" # noqa: S608
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
)
payments = result.mappings().all()
logger.info(f"Payments count: {len(payments)}")
@@ -640,12 +629,11 @@ async def m027_update_apipayments_data(db: Connection):
tag = extra.get("tag")
tsph = db.timestamp_placeholder("created_at")
await db.execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
UPDATE apipayments
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
WHERE checking_id = :checking_id
""", # noqa: S608
""",
{
"tag": tag,
"created_at": created_at,
+4 -4
View File
@@ -211,8 +211,7 @@ class ExtensionRelease(BaseModel):
self, amount: int | None = None
) -> ReleasePaymentInfo | None:
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
if not url:
raise ValueError("Missing URL for payment info.")
assert url, "Missing URL for payment info."
try:
async with httpx.AsyncClient() as client:
resp = await client.get(url)
@@ -377,8 +376,9 @@ class InstallableExtension(BaseModel):
if ext_zip_file.is_file():
os.remove(ext_zip_file)
try:
if not self.meta or not self.meta.installed_release:
raise ValueError("No installed release.")
assert (
self.meta and self.meta.installed_release
), "installed_release is none."
self._restore_payment_info()
-1
View File
@@ -10,7 +10,6 @@ class CreateLnurl(BaseModel):
comment: Optional[str] = None
description: Optional[str] = None
unit: Optional[str] = None
internal_memo: Optional[str] = None
class CreateLnurlAuth(BaseModel):
-2
View File
@@ -10,7 +10,6 @@ from .payments import (
check_wallet_limits,
create_fiat_invoice,
create_invoice,
create_payment_request,
create_wallet_invoice,
fee_reserve,
fee_reserve_total,
@@ -42,7 +41,6 @@ __all__ = [
"check_webpush_settings",
"create_fiat_invoice",
"create_invoice",
"create_payment_request",
"create_user_account",
"create_user_account_no_ckeck",
"create_wallet_invoice",
+7 -7
View File
@@ -26,11 +26,10 @@ async def install_extension(ext_info: InstallableExtension) -> Extension:
ext_info.meta = ext_info.meta or ExtensionMeta()
if (
ext_info.meta.installed_release
and not ext_info.meta.installed_release.is_version_compatible
):
raise ValueError("Incompatible extension version")
if ext_info.meta.installed_release:
assert (
ext_info.meta.installed_release.is_version_compatible
), "Incompatible extension version"
installed_ext = await get_installed_extension(ext_info.id)
if installed_ext and installed_ext.meta:
@@ -94,8 +93,9 @@ async def stop_extension_background_work(ext_id: str) -> bool:
old_module = importlib.import_module(ext.module_name)
stop_fn_name = f"{ext_id}_stop"
if not hasattr(old_module, stop_fn_name):
raise ValueError(f"No stop function found for '{ext.module_name}'.")
assert hasattr(
old_module, stop_fn_name
), f"No stop function found for '{ext.module_name}'."
stop_fn = getattr(old_module, stop_fn_name)
if stop_fn:
+5 -6
View File
@@ -67,16 +67,16 @@ async def redeem_lnurl_withdraw(
external=True,
wal=wallet_id,
)
except Exception as exc:
logger.debug(exc)
except Exception:
pass
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(res["callback"])
await client.get(res["callback"], params=params)
except Exception as exc:
logger.debug(exc)
except Exception:
pass
async def perform_lnurlauth(
@@ -136,8 +136,7 @@ async def perform_lnurlauth(
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
if not key.verifying_key:
raise ValueError("LNURLauth verifying_key does not exist")
assert key.verifying_key, "LNURLauth verifying_key does not exist"
check_callback_url(callback)
r = await client.get(
callback,
+3 -3
View File
@@ -28,7 +28,7 @@ from lnbits.helpers import check_callback_url, is_valid_email_address
from lnbits.settings import settings
from lnbits.utils.nostr import normalize_private_key
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
notifications_queue: asyncio.Queue = asyncio.Queue()
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
@@ -42,8 +42,8 @@ def enqueue_notification(message_type: NotificationType, values: dict) -> None:
logger.error(f"Error enqueuing notification: {e}")
async def process_next_notification() -> None:
notification_message = await notifications_queue.get()
async def process_next_notification():
notification_message: NotificationMessage = await notifications_queue.get()
message_type, text = _notification_message_to_text(notification_message)
await send_notification(text, message_type)
+23 -75
View File
@@ -66,8 +66,7 @@ async def pay_invoice(
if settings.lnbits_only_allow_incoming_payments:
raise PaymentError("Only incoming payments allowed.", status="failed")
invoice = _validate_payment_request(payment_request, max_sat)
if not invoice.amount_msat:
raise ValueError("Missig invoice amount.")
assert invoice.amount_msat
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
amount_msat = invoice.amount_msat
@@ -100,18 +99,6 @@ async def pay_invoice(
return payment
async def create_payment_request(
wallet_id: str, invoice_data: CreateInvoice
) -> Payment:
"""
Create a lightning invoice or a fiat payment request.
"""
if invoice_data.fiat_provider:
return await create_fiat_invoice(wallet_id, invoice_data)
return await create_wallet_invoice(wallet_id, invoice_data)
async def create_fiat_invoice(
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
):
@@ -301,7 +288,6 @@ async def create_invoice(
memo=memo,
extra=extra,
webhook=webhook,
fee=payment_response.fee_msat or 0,
)
payment = await create_payment(
@@ -323,45 +309,17 @@ async def update_pending_payments(wallet_id: str):
await update_pending_payment(payment)
async def update_pending_payment(payment: Payment) -> Payment:
async def update_pending_payment(payment: Payment) -> bool:
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
elif status.success:
payment = await update_payment_success_status(payment, status)
return payment
async def check_pending_payments():
"""
check_pending_payments is called during startup to check for pending payments with
the backend and also to delete expired invoices. Incoming payments will be
checked only once, outgoing pending payments will be checked regularly.
"""
funding_source = get_funding_source()
if funding_source.__class__.__name__ == "VoidWallet":
logger.warning("Task: skipping pending check for VoidWallet")
return
start_time = time.time()
pending_payments = await get_payments(
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
complete=False,
pending=True,
exclude_uncheckable=True,
)
count = len(pending_payments)
if count > 0:
logger.info(f"Task: checking {count} pending payments of last 15 days...")
for i, payment in enumerate(pending_payments):
payment = await update_pending_payment(payment)
prefix = f"payment ({i+1} / {count})"
logger.debug(f"{prefix} {payment.status} {payment.checking_id}")
await asyncio.sleep(0.01) # to avoid complete blocking
logger.info(
f"Task: pending check finished for {count} payments"
f" (took {time.time() - start_time:0.3f} s)"
)
return True
if status.success:
payment.status = PaymentState.SUCCESS
await update_payment(payment)
return True
return False
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
@@ -751,20 +709,19 @@ async def _pay_external_invoice(
)
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
service_fee_msat = service_fee(amount_msat, internal=False)
task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
)
# make sure a hold invoice or deferred payment is not blocking the server
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
try:
payment_response = await asyncio.wait_for(task, timeout=wait_time)
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
payment_response = await asyncio.wait_for(task, wait_time)
except asyncio.TimeoutError:
# return pending payment on timeout
logger.debug(
f"payment timeout after {wait_time}s, {checking_id} is still pending"
)
logger.debug(f"payment timeout, {checking_id} is still pending")
return payment
# payment failed
@@ -778,28 +735,18 @@ async def _pay_external_invoice(
message = payment_response.error_message or "without an error message."
raise PaymentError(f"Payment failed: {message}", status="failed")
if payment_response.success:
payment = await update_payment_success_status(
payment, payment_response, conn=conn
)
# payment.ok can be True (paid) or None (pending)!
payment.status = (
PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING
)
payment.fee = -(abs(payment_response.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment_response.preimage
await update_payment(payment, payment_response.checking_id, conn=conn)
payment.checking_id = payment_response.checking_id
if payment.success:
await send_payment_notification(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
return payment
async def update_payment_success_status(
payment: Payment,
status: PaymentStatus,
conn: Optional[Connection] = None,
) -> Payment:
if status.success:
service_fee_msat = service_fee(payment.amount, internal=False)
payment.status = PaymentState.SUCCESS
payment.fee = -(abs(status.fee_msat or 0) + abs(service_fee_msat))
payment.preimage = payment.preimage or status.preimage
await update_payment(payment, conn=conn)
return payment
@@ -833,7 +780,8 @@ async def _verify_external_payment(
if status.success:
# payment was successful on the fundingsource
await update_payment_success_status(payment, status, conn=conn)
payment.status = PaymentState.SUCCESS
await update_payment(payment, conn=conn)
raise PaymentError(
"Failed payment was already paid on the fundingsource.",
status="success",
+1 -2
View File
@@ -19,8 +19,7 @@ async def check_webpush_settings():
vapid = Vapid()
vapid.generate_keys()
privkey = vapid.private_pem()
if not vapid.public_key:
raise ValueError("VAPID public key does not exist")
assert vapid.public_key, "VAPID public key does not exist"
pubkey = b64urlencode(
vapid.public_key.public_bytes(
serialization.Encoding.X962,
+1 -2
View File
@@ -80,8 +80,7 @@ async def create_user_account_no_ckeck(
logger.error(f"Error enabeling default extension {ext_id}: {e}")
user = await get_user_from_account(account)
if not user:
raise ValueError("Cannot find user for account.")
assert user, "Cannot find user for account."
return user
+10 -10
View File
@@ -13,7 +13,7 @@ from lnbits.core.crud.audit import delete_expired_audit_entries
from lnbits.core.crud.payments import get_payments_status_count
from lnbits.core.crud.users import get_accounts
from lnbits.core.crud.wallets import get_wallets_count
from lnbits.core.models.audit import AuditEntry
from lnbits.core.models import AuditEntry
from lnbits.core.models.extensions import InstallableExtension
from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.funding_source import (
@@ -31,10 +31,10 @@ from lnbits.settings import settings
from lnbits.tasks import create_unique_task
from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue[AuditEntry] = asyncio.Queue()
audit_queue: asyncio.Queue = asyncio.Queue()
async def run_by_the_minute_tasks() -> None:
async def run_by_the_minute_tasks():
minute_counter = 0
while settings.lnbits_running:
status_minutes = settings.lnbits_notification_server_status_hours * 60
@@ -69,7 +69,7 @@ async def run_by_the_minute_tasks() -> None:
await asyncio.sleep(60)
async def _notify_server_status() -> None:
async def _notify_server_status():
accounts = await get_accounts(filters=Filters(limit=0))
wallets_count = await get_wallets_count()
payments = await get_payments_status_count()
@@ -90,7 +90,7 @@ async def _notify_server_status() -> None:
enqueue_notification(NotificationType.server_status, values)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
"""
This worker dispatches events to all extensions and dispatches webhooks.
"""
@@ -103,13 +103,13 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
await send_payment_notification(wallet, payment)
async def wait_for_audit_data() -> None:
async def wait_for_audit_data():
"""
Waits for audit entries to be pushed to the queue.
Then it inserts the entries into the DB.
"""
while settings.lnbits_running:
data = await audit_queue.get()
data: AuditEntry = await audit_queue.get()
try:
await create_audit_entry(data)
except Exception as ex:
@@ -117,7 +117,7 @@ async def wait_for_audit_data() -> None:
await asyncio.sleep(3)
async def wait_notification_messages() -> None:
async def wait_notification_messages():
while settings.lnbits_running:
try:
@@ -127,7 +127,7 @@ async def wait_notification_messages() -> None:
await asyncio.sleep(3)
async def purge_audit_data() -> None:
async def purge_audit_data():
"""
Remove audit entries which have passed their retention period.
"""
@@ -141,7 +141,7 @@ async def purge_audit_data() -> None:
await asyncio.sleep(60 * 60)
async def collect_exchange_rates_data() -> None:
async def collect_exchange_rates_data():
"""
Collect exchange rates data. Used for monitoring only.
"""
+8 -10
View File
@@ -145,9 +145,7 @@
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
@@ -156,9 +154,9 @@
<br />
</div>
</div>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12" v-text="$t('ui_default_theme')"></div>
<div class="col-12 col-sm-6 col-lg-3">
<p><span v-text="$t('ui_default_theme')"></span></p>
<div class="row q-col-gutter-md">
<div class="col">
<q-select
v-model="formData.lnbits_default_border"
:options="globalBorderOptions"
@@ -167,7 +165,7 @@
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="col">
<q-select
v-model="formData.lnbits_default_theme"
:options="lnbits_theme_options"
@@ -176,7 +174,7 @@
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="col">
<q-select
v-model="formData.lnbits_default_reaction"
:options="reactionOptions"
@@ -185,7 +183,7 @@
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="col">
<q-input
type="text"
v-model="formData.lnbits_default_bgimage"
@@ -195,7 +193,7 @@
>
</q-input>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="col">
<q-toggle
type="bool"
v-model="formData.lnbits_default_gradient"
+3 -76
View File
@@ -667,36 +667,8 @@
dense
type="textarea"
rows="2"
v-model="receive.data.memo"
v-model.trim="receive.data.memo"
:label="$t('memo')"
>
<template v-if="receive.data.internalMemo === null" v-slot:append>
<q-icon
name="add_comment"
@click.stop.prevent="receive.data.internalMemo = ''"
class="cursor-pointer"
></q-icon>
<q-tooltip>
<span v-text="$t('internal_memo')"></span>
</q-tooltip>
</template>
</q-input>
<q-input
v-if="receive.data.internalMemo !== null"
autogrow
filled
dense
v-model="receive.data.internalMemo"
class="q-mb-lg"
:label="$t('internal_memo')"
:hint="$t('internal_memo_hint_receive')"
:rules="[ val => !val || val.length <= 512 || 'Please use maximum 512 characters' ]"
><template v-slot:append>
<q-icon
name="cancel"
@click.stop.prevent="receive.data.internalMemo = null"
class="cursor-pointer"
/> </template
></q-input>
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
<q-list bordered dense class="rounded-borders">
@@ -876,21 +848,6 @@
</div>
<q-separator></q-separator>
<h6 class="text-center" v-text="parse.invoice.description"></h6>
<q-input
autogrow
filled
dense
v-model="parse.data.internalMemo"
:label="$t('internal_memo')"
:hint="$t('internal_memo_hint_pay')"
class="q-mb-lg"
:rules="[ val => !val || val.length <= 512 || 'Please use maximum 512 characters' ]"
><template v-if="parse.data.internalMemo" v-slot:append>
<q-icon
name="cancel"
@click.stop.prevent="parse.data.internalMemo = null"
class="cursor-pointer" /></template
></q-input>
<q-list separator bordered dense class="q-mb-md">
<q-expansion-item expand-separator icon="info" label="Details">
<q-list separator>
@@ -1084,7 +1041,7 @@
</p>
</div>
<div class="row">
<div class="col q-mb-lg">
<div class="col">
<q-select
filled
dense
@@ -1118,39 +1075,9 @@
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 512 ? 'textarea' : 'text'"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
><template
v-if="parse.data.internalMemo === null"
v-slot:append
>
<q-icon
name="add_comment"
@click.stop.prevent="parse.data.internalMemo = ''"
class="cursor-pointer"
></q-icon>
<q-tooltip>
<span v-text="$t('internal_memo')"></span>
</q-tooltip> </template
></q-input>
<br />
<q-input
v-if="parse.data.internalMemo !== null"
autogrow
filled
dense
v-model="parse.data.internalMemo"
:label="$t('internal_memo')"
:hint="$t('internal_memo_hint_pay')"
class=""
:rules="[ val => !val || val.length <= 512 || 'Please use maximum 512 characters' ]"
><template v-slot:append>
<q-icon
name="cancel"
@click.stop.prevent="parse.data.internalMemo = null"
class="cursor-pointer"
/> </template
></q-input>
</div>
</div>
+1 -2
View File
@@ -85,8 +85,7 @@ async def api_update_settings(data: UpdateSettings, user: User = Depends(check_a
enqueue_notification(NotificationType.settings_update, {"username": user.username})
await update_admin_settings(data)
admin_settings = await get_admin_settings(user.super_user)
if not admin_settings:
raise ValueError("Updated admin settings not found.")
assert admin_settings, "Updated admin settings not found."
update_cached_settings(admin_settings.dict())
core_app_extra.register_new_ratelimiter()
return {"status": "Success"}
+2 -3
View File
@@ -93,7 +93,7 @@ async def api_create_account(data: CreateWallet) -> Wallet:
@api_router.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan( # noqa: C901
async def api_lnurlscan(
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
try:
@@ -124,8 +124,7 @@ async def api_lnurlscan( # noqa: C901
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
if not lnurlauth_key.verifying_key:
raise ValueError("LNURL auth key not found for this domain.")
assert lnurlauth_key.verifying_key
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
headers = {"User-Agent": settings.user_agent}
+28 -53
View File
@@ -191,14 +191,12 @@ async def api_create_user_api_token(
data: ApiTokenRequest,
user: User = Depends(check_user_exists),
) -> ApiTokenResponse:
if not data.expiration_time_minutes > 0:
raise ValueError("Expiration time must be in the future.")
assert data.expiration_time_minutes > 0, "Expiration time must be in the future."
account = await get_account(user.id)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
if not account.username:
raise ValueError("Username must be configured.")
assert account.username, "Username must be configured."
acls = await get_user_access_control_lists(user.id)
acl = acls.get_acl_by_id(data.acl_id)
@@ -225,8 +223,7 @@ async def api_delete_user_api_token(
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
if not account.username:
raise ValueError("Username must be configured.")
assert account.username, "Username must be configured."
acls = await get_user_access_control_lists(user.id)
acl = acls.get_acl_by_id(data.acl_id)
@@ -321,7 +318,7 @@ async def update_pubkey(
payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]:
if data.user_id != user.id:
raise ValueError("Invalid user ID.")
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
_validate_auth_timeout(payload.auth_time)
if (
@@ -329,7 +326,7 @@ async def update_pubkey(
and data.pubkey != user.pubkey
and await get_account_by_pubkey(data.pubkey)
):
raise ValueError("Public key already in use.")
raise HTTPException(HTTPStatus.BAD_REQUEST, "Public key already in use.")
account = await get_account(user.id)
if not account:
@@ -347,8 +344,7 @@ async def update_password(
payload: AccessTokenPayload = Depends(access_token_payload),
) -> Optional[User]:
_validate_auth_timeout(payload.auth_time)
if data.user_id != user.id:
raise ValueError("Invalid user ID.")
assert data.user_id == user.id, "Invalid user ID."
if (
data.username
and user.username != data.username
@@ -357,15 +353,12 @@ async def update_password(
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
account = await get_account(user.id)
if not account:
raise ValueError("Account not found.")
assert account, "Account not found."
# old accounts do not have a password
if account.password_hash:
if not data.password_old:
raise ValueError("Missing old password.")
if not account.verify_password(data.password_old):
raise ValueError("Invalid old password.")
assert data.password_old, "Missing old password."
assert account.verify_password(data.password_old), "Invalid old password."
account.username = data.username
account.hash_password(data.password)
@@ -383,10 +376,8 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
)
if data.password != data.password_repeat:
raise ValueError("Passwords do not match.")
if not data.reset_key[:10].startswith("reset_key_"):
raise ValueError("This is not a reset key.")
assert data.password == data.password_repeat, "Passwords do not match."
assert data.reset_key[:10].startswith("reset_key_"), "This is not a reset key."
try:
reset_key = base64.b64decode(data.reset_key[10:]).decode()
@@ -394,16 +385,12 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
except Exception as exc:
raise ValueError("Invalid reset key.") from exc
if not reset_data_json:
raise ValueError("Cannot process reset key.")
assert reset_data_json, "Cannot process reset key."
action, user_id, request_time = json.loads(reset_data_json)
if not action:
raise ValueError("Missing action.")
if not user_id:
raise ValueError("Missing user ID.")
if not request_time:
raise ValueError("Missing reset time.")
assert action, "Missing action."
assert user_id, "Missing user ID."
assert request_time, "Missing reset time."
_validate_auth_timeout(request_time)
@@ -570,8 +557,8 @@ def _find_auth_provider_class(provider: str) -> Callable:
provider_class = getattr(provider_module, f"{provider.title()}SSO")
if provider_class:
return provider_class
except Exception as exc:
logger.debug(exc)
except Exception:
pass
raise ValueError(f"No SSO provider found for '{provider}'.")
@@ -589,40 +576,28 @@ def _nostr_nip98_event(request: Request) -> dict:
event = json.loads(event_json)
except Exception as exc:
logger.warning(exc)
if not event:
raise ValueError("Nostr login event cannot be parsed.")
assert event, "Nostr login event cannot be parsed."
if not verify_event(event):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.")
if not event["kind"] == 27_235:
raise ValueError("Invalid event kind.")
assert event["kind"] == 27_235, "Invalid event kind."
auth_threshold = settings.auth_credetials_update_threshold
if not (abs(time() - event["created_at"]) < auth_threshold):
raise ValueError(
f"More than {auth_threshold} seconds have passed "
"since the event was signed."
)
assert (
abs(time() - event["created_at"]) < auth_threshold
), f"More than {auth_threshold} seconds have passed since the event was signed."
_check_nostr_event_tags(event)
return event
def _check_nostr_event_tags(event: dict):
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
if not method:
raise ValueError("Tag 'method' is missing.")
if not method.upper() == "POST":
raise ValueError("Invalid value for tag 'method'.")
assert method, "Tag 'method' is missing."
assert method.upper() == "POST", "Invalid value for tag 'method'."
url = next((v for k, v in event["tags"] if k == "u"), None)
if not url:
raise ValueError("Tag 'u' for URL is missing.")
assert url, "Tag 'u' for URL is missing."
accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls]
if url not in accepted_urls:
raise ValueError(f"Invalid value for tag 'u': '{url}'.")
assert url in accepted_urls, f"Invalid value for tag 'u': '{url}'."
return event
def _validate_auth_timeout(auth_time: Optional[int] = 0):
+78 -63
View File
@@ -170,52 +170,64 @@ async def api_enable_extension(
raise HTTPException(
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
)
try:
logger.info(f"Enabling extension: {ext_id}.")
ext = await get_installed_extension(ext_id)
assert ext, f"Extension '{ext_id}' is not installed."
assert ext.active, f"Extension '{ext_id}' is not activated."
logger.info(f"Enabling extension: {ext_id}.")
ext = await get_installed_extension(ext_id)
if not ext:
raise ValueError(f"Extension '{ext_id}' is not installed.")
if not ext.active:
raise ValueError(f"Extension '{ext_id}' is not activated.")
user_ext = await get_user_extension(user.id, ext_id)
if not user_ext:
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
await create_user_extension(user_ext)
user_ext = await get_user_extension(user.id, ext_id)
if not user_ext:
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
await create_user_extension(user_ext)
if user.admin or not ext.requires_payment:
user_ext.active = True
await update_user_extension(user_ext)
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
if user.admin or not ext.requires_payment:
user_ext.active = True
await update_user_extension(user_ext)
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
raise HTTPException(
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
)
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
raise HTTPException(
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
if user_ext.is_paid:
user_ext.active = True
await update_user_extension(user_ext)
return SimpleStatus(
success=True, message=f"Paid extension '{ext_id}' enabled."
)
assert (
ext.meta and ext.meta.pay_to_enable and ext.meta.pay_to_enable.wallet
), f"Extension '{ext_id}' is missing payment wallet."
payment_status = await check_transaction_status(
wallet_id=ext.meta.pay_to_enable.wallet,
payment_hash=user_ext.extra.payment_hash_to_enable,
)
if user_ext.is_paid:
if not payment_status.paid:
raise HTTPException(
HTTPStatus.PAYMENT_REQUIRED,
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
)
user_ext.active = True
user_ext.extra.paid_to_enable = True
await update_user_extension(user_ext)
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
raise ValueError(f"Extension '{ext_id}' is missing payment wallet.")
payment_status = await check_transaction_status(
wallet_id=ext.meta.pay_to_enable.wallet,
payment_hash=user_ext.extra.payment_hash_to_enable,
)
if not payment_status.paid:
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except HTTPException as exc:
raise exc from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(
HTTPStatus.PAYMENT_REQUIRED,
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
)
user_ext.active = True
user_ext.extra.paid_to_enable = True
await update_user_extension(user_ext)
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=(f"Failed to enable '{ext_id}' "),
) from exc
@extension_router.put("/{ext_id}/disable")
@@ -243,8 +255,7 @@ async def api_activate_extension(ext_id: str) -> SimpleStatus:
logger.info(f"Activating extension: '{ext_id}'.")
ext = await get_valid_extension(ext_id)
if not ext:
raise ValueError(f"Extension '{ext_id}' doesn't exist.")
assert ext, f"Extension '{ext_id}' doesn't exist."
await activate_extension(ext)
return SimpleStatus(success=True, message=f"Extension '{ext_id}' activated.")
@@ -263,8 +274,7 @@ async def api_deactivate_extension(ext_id: str) -> SimpleStatus:
logger.info(f"Deactivating extension: '{ext_id}'.")
ext = await get_valid_extension(ext_id)
if not ext:
raise ValueError(f"Extension '{ext_id}' doesn't exist.")
assert ext, f"Extension '{ext_id}' doesn't exist."
await deactivate_extension(ext_id)
return SimpleStatus(success=True, message=f"Extension '{ext_id}' deactivated.")
@@ -344,35 +354,40 @@ async def get_extension_releases(ext_id: str) -> list[ExtensionRelease]:
async def get_pay_to_install_invoice(
ext_id: str, data: CreateExtension
) -> ReleasePaymentInfo:
if ext_id != data.ext_id:
raise ValueError(
f"Wrong extension id. Expected {ext_id}, but got {data.ext_id}"
try:
assert (
ext_id == data.ext_id
), f"Wrong extension id. Expected {ext_id}, but got {data.ext_id}"
assert data.cost_sats, "A non-zero amount must be specified."
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
if not data.cost_sats:
raise ValueError("A non-zero amount must be specified.")
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
if not release:
raise ValueError("Release not found.")
if not release.pay_link:
raise ValueError("Pay link not found for release.")
assert release, "Release not found."
assert release.pay_link, "Pay link not found for release."
payment_info = await release.fetch_release_payment_info(data.cost_sats)
payment_info = await release.fetch_release_payment_info(data.cost_sats)
if not (payment_info and payment_info.payment_request):
raise ValueError("Cannot request invoice.")
invoice = bolt11_decode(payment_info.payment_request)
assert payment_info and payment_info.payment_request, "Cannot request invoice."
invoice = bolt11_decode(payment_info.payment_request)
if invoice.amount_msat is None:
raise ValueError("Invoic amount is missing.")
invoice_amount = int(invoice.amount_msat / 1000)
if invoice_amount != data.cost_sats:
raise ValueError(f"Wrong invoice amount: {invoice_amount}.")
if payment_info.payment_hash != invoice.payment_hash:
raise ValueError("Wrong invoice payment hash.")
assert invoice.amount_msat is not None, "Invoic amount is missing."
invoice_amount = int(invoice.amount_msat / 1000)
assert (
invoice_amount == data.cost_sats
), f"Wrong invoice amount: {invoice_amount}."
assert (
payment_info.payment_hash == invoice.payment_hash
), "Wrong invoice payment hash."
return payment_info
return payment_info
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice"
) from exc
@extension_router.put("/{ext_id}/invoice/enable")
+42 -81
View File
@@ -60,10 +60,10 @@ from ..crud import (
get_payments_paginated,
get_standalone_payment,
get_wallet_for_key,
update_payment_extra,
)
from ..services import (
create_payment_request,
create_fiat_invoice,
create_wallet_invoice,
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
@@ -121,6 +121,7 @@ async def api_payments_counting_stats(
filters: Filters[PaymentFilters] = 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
@@ -141,6 +142,7 @@ async def api_payments_wallets_stats(
filters: Filters[PaymentFilters] = 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
@@ -161,6 +163,7 @@ async def api_payments_daily_stats(
user: User = Depends(check_user_exists),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
if user.admin:
# admin user can see payments from all wallets
for_user_id = None
@@ -261,39 +264,10 @@ async def api_payments_create(
)
# If the payment is not outgoing, we can create a new invoice.
return await create_payment_request(wallet_id, invoice_data)
if invoice_data.fiat_provider:
return await create_fiat_invoice(wallet_id, invoice_data)
@payment_router.patch(
"/extra/{payment_hash}", description="Update extra data for a payment"
)
async def api_payments_update_extra(
payment_hash: str,
extra: dict,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Payment:
payment = await get_standalone_payment(payment_hash, wallet_id=wallet.wallet.id)
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
if not isinstance(extra, dict):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Extra data must be a dictionary.",
)
if payment.extra is None:
payment.extra = {}
for key, value in extra.items():
if key in payment.extra:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Key '{key}' already exists in extra data.",
)
payment.extra[key] = value
return await update_payment_extra(payment)
return await create_wallet_invoice(wallet_id, invoice_data)
@payment_router.get("/fee-reserve")
@@ -311,36 +285,23 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
)
def _validate_lnurl_response(
params: dict, domain: str, amount_msat: int
) -> bolt11.Invoice:
if params.get("status") == "ERROR":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != amount_msat:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"{domain} returned an invalid invoice. Expected"
f" {amount_msat} msat, got {invoice.amount_msat}."
),
)
return invoice
@payment_router.post("/lnurl")
async def api_payments_pay_lnurl(
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Payment:
domain = urlparse(data.callback).netloc
async def _fetch_lnurl_params(data: CreateLnurl, amount_msat: int, domain: str) -> dict:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
try:
r: httpx.Response = await client.get(
if data.unit and data.unit != "sat":
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
# no msat precision
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
check_callback_url(data.callback)
r = await client.get(
data.callback,
params={"amount": amount_msat, "comment": data.comment},
timeout=40,
@@ -354,24 +315,29 @@ async def _fetch_lnurl_params(data: CreateLnurl, amount_msat: int, domain: str)
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
) from exc
return json.loads(r.text)
params = json.loads(r.text)
if params.get("status") == "ERROR":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
@payment_router.post("/lnurl")
async def api_payments_pay_lnurl(
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Payment:
domain = urlparse(data.callback).netloc
check_callback_url(data.callback)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
if data.unit and data.unit != "sat":
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
amount_msat = ceil(amount_msat // 1000) * 1000
else:
amount_msat = data.amount
params = await _fetch_lnurl_params(data, amount_msat, domain)
_validate_lnurl_response(params, domain, amount_msat)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != amount_msat:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"{domain} returned an invalid invoice. Expected"
f" {amount_msat} msat, got {invoice.amount_msat}."
),
)
extra = {}
if params.get("successAction"):
@@ -381,12 +347,7 @@ async def api_payments_pay_lnurl(
if data.unit and data.unit != "sat":
extra["fiat_currency"] = data.unit
extra["fiat_amount"] = data.amount / 1000
if data.internal_memo is not None:
if len(data.internal_memo) > 512:
raise ValueError("Internal memo must be 512 characters or less.")
extra["internal_memo"] = data.internal_memo
if data.description is None:
raise ValueError("Description is required")
assert data.description is not None, "description is required"
payment = await pay_invoice(
wallet_id=wallet.wallet.id,
+1 -2
View File
@@ -187,8 +187,7 @@ async def api_users_reset_password(user_id: str) -> str:
reset_data = ["reset", user_id, int(time.time())]
reset_data_json = json.dumps(reset_data, separators=(",", ":"), ensure_ascii=False)
reset_key = encrypt_internal_message(reset_data_json)
if not reset_key:
raise ValueError("Cannot generate reset key.")
assert reset_key, "Cannot generate reset key."
reset_key_b64 = base64.b64encode(reset_key.encode()).decode()
return f"reset_key_{reset_key_b64}"
+4 -4
View File
@@ -252,7 +252,7 @@ class Connection(Compat):
{clause}
{group_by_string}
) as count
""", # noqa: S608
""",
parsed_values,
)
row = result.mappings().first()
@@ -597,7 +597,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields = ", ".join([f'"{key}"' for key in keys])
values = ", ".join(placeholders)
return f"INSERT INTO {table_name} ({fields}) VALUES ({values})" # noqa: S608
return f"INSERT INTO {table_name} ({fields}) VALUES ({values})"
def update_query(
@@ -615,7 +615,7 @@ def update_query(
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields.append(f'"{field}" = {placeholder}')
query = ", ".join(fields)
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
return f"UPDATE {table_name} SET {query} {where}"
def model_to_dict(model: BaseModel) -> dict:
@@ -658,7 +658,7 @@ def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
return dict_to_model(_subdict, model)
def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
"""
Convert a dictionary with JSON-encoded nested models to a Pydantic model
:param _dict: Dictionary from database
+1 -1
View File
@@ -69,7 +69,7 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
)
def register_exception_handlers(app: FastAPI): # noqa: C901
def register_exception_handlers(app: FastAPI):
"""Register exception handlers for the FastAPI app"""
@app.exception_handler(Exception)
+2 -5
View File
@@ -306,11 +306,8 @@ def check_callback_url(url: str):
)
def download_url(url: str, save_path: Path):
if not url.startswith(("http:", "https:")):
raise ValueError(f"Invalid URL: {url}. Must start with 'http' or 'https'.")
with request.urlopen(url, timeout=60) as dl_file: # noqa: S310
def download_url(url, save_path):
with request.urlopen(url, timeout=60) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
+1
View File
@@ -131,6 +131,7 @@ class AuditMiddleware(BaseHTTPMiddleware):
try:
response = await call_next(request)
assert response
return response
finally:
if request_details:
+7 -30
View File
@@ -201,11 +201,10 @@ class InstalledExtensionsSettings(LNbitsSettings):
if r.find_in_conflict(ext_redirect_paths)
}
if len(existing_redirects) != 0:
raise ValueError(
f"Cannot redirect for extension '{ext_id}'."
f" Already mapped by {existing_redirects}."
)
assert len(existing_redirects) == 0, (
f"Cannot redirect for extension '{ext_id}'."
f" Already mapped by {existing_redirects}."
)
self._remove_extension_redirects(ext_id)
self.lnbits_extensions_redirects += ext_redirect_paths
@@ -256,7 +255,7 @@ class ThemesSettings(LNbitsSettings):
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
lnbits_ad_space_title: str = Field(default="Supported by")
lnbits_ad_space: str = Field(
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
) # sneaky sneaky
lnbits_ad_space_enabled: bool = Field(default=False)
lnbits_allowed_currencies: list[str] = Field(default=[])
@@ -306,7 +305,7 @@ class FeeSettings(LNbitsSettings):
return 0
reserve_min = self.lnbits_reserve_fee_min
reserve_percent = self.lnbits_reserve_fee_percent
return max(int(reserve_min), int(abs(amount_msat) * reserve_percent / 100.0))
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
class ExchangeProvidersSettings(LNbitsSettings):
@@ -449,18 +448,6 @@ class ClicheFundingSource(LNbitsSettings):
cliche_endpoint: str | None = Field(default=None)
class CLNRestFundingSource(LNbitsSettings):
clnrest_url: str | None = Field(default=None)
clnrest_ca: str | None = Field(default=None)
clnrest_cert: str | None = Field(default=None)
clnrest_readonly_rune: str | None = Field(default=None)
clnrest_invoice_rune: str | None = Field(default=None)
clnrest_pay_rune: str | None = Field(default=None)
clnrest_renepay_rune: str | None = Field(default=None)
clnrest_last_pay_index: str | None = Field(default=None)
clnrest_nodeid: str | None = Field(default=None)
class CoreLightningFundingSource(LNbitsSettings):
corelightning_rpc: str | None = Field(default=None)
corelightning_pay_command: str = Field(default="pay")
@@ -562,12 +549,6 @@ class BreezSdkFundingSource(LNbitsSettings):
breez_use_trampoline: bool = Field(default=True)
class BreezLiquidSdkFundingSource(LNbitsSettings):
breez_liquid_api_key: str | None = Field(default=None)
breez_liquid_seed: str | None = Field(default=None)
breez_liquid_fee_offset_sat: int = Field(default=50)
class BoltzFundingSource(LNbitsSettings):
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
boltz_client_macaroon: str | None = Field(default=None)
@@ -620,7 +601,6 @@ class FundingSourcesSettings(
FakeWalletFundingSource,
LNbitsFundingSource,
ClicheFundingSource,
CLNRestFundingSource,
CoreLightningFundingSource,
CoreLightningRestFundingSource,
EclairFundingSource,
@@ -638,7 +618,6 @@ class FundingSourcesSettings(
NWCFundingSource,
BreezSdkFundingSource,
StrikeFundingSource,
BreezLiquidSdkFundingSource,
):
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
# How long to wait for the payment to be confirmed before returning a pending status
@@ -697,7 +676,7 @@ class NodeUISettings(LNbitsSettings):
class AuthMethods(Enum):
user_id_only = "user-id-only"
username_and_password = "username-password" # noqa: S105
username_and_password = "username-password"
nostr_auth_nip98 = "nostr-auth-nip98"
google_auth = "google-auth"
github_auth = "github-auth"
@@ -945,8 +924,6 @@ class SuperUserSettings(LNbitsSettings):
"BoltzWallet",
"BlinkWallet",
"BreezSdkWallet",
"BreezLiquidSdkWallet",
"CLNRestWallet",
"CoreLightningRestWallet",
"CoreLightningWallet",
"EclairWallet",
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -4
View File
@@ -62,6 +62,14 @@ body[data-theme=monochrome].neon-border .q-date--dark {
box-shadow: none;
}
body[data-theme=salvador].neon-border .q-card,
body[data-theme=salvador].neon-border .q-card.q-card--dark,
body[data-theme=salvador].neon-border .q-date,
body[data-theme=salvador].neon-border .q-date--dark {
border: 2px solid #1976d2;
box-shadow: none;
}
body.hard-border .q-card,
body.hard-border .q-card.q-card--dark,
body.hard-border .q-date,
@@ -109,21 +117,21 @@ body[data-theme=bitcoin] [data-theme=bitcoin] .q-stepper--dark {
body[data-theme=freedom] {
--q-primary: #e22156;
--q-secondary: #b91a45;
--q-dark-page: #0a0a0a;
--q-dark-page: #462f36;
}
body[data-theme=freedom] [data-theme=freedom] .q-card--dark,
body[data-theme=freedom] [data-theme=freedom] .q-stepper--dark {
background: #1b1b1b !important;
background: #47393d !important;
}
body[data-theme=cyber] {
--q-primary: #7cb342;
--q-secondary: #558b2f;
--q-dark-page: #0a0a0a;
--q-dark-page: #000;
}
body[data-theme=cyber] [data-theme=cyber] .q-card--dark,
body[data-theme=cyber] [data-theme=cyber] .q-stepper--dark {
background: #1b1b1b !important;
background: #1f2915 !important;
}
body[data-theme=mint] {
@@ -166,6 +174,16 @@ body[data-theme=monochrome] [data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
body[data-theme=salvador] {
--q-primary: #1976d2;
--q-secondary: #26a69a;
--q-dark-page: #253647;
}
body[data-theme=salvador] [data-theme=salvador] .q-card--dark,
body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
background: #343d47 !important;
}
body.gradient-bg {
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
background-attachment: fixed;
-5
View File
@@ -101,11 +101,6 @@ window.localisation.en = {
memo: 'Memo',
date: 'Date',
path: 'Path',
internal_memo: 'Internal memo (optional)',
internal_memo_hint_receive:
"This memo is not shown to the payer but it's stored in the invoice for your reference.",
internal_memo_hint_pay:
"This memo is not shown to the payee but it's stored in the payment for your reference.",
payment_processing: 'Processing payment...',
payment_processing: 'Processing payment...',
payment_successful: 'Payment successful!',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

+10 -35
View File
@@ -20,35 +20,22 @@ window.LNbits = {
memo,
unit = 'sat',
lnurlCallback = null,
fiatProvider = null,
internalMemo = null
fiatProvider = null
) {
const data = {
return this.request('post', '/api/v1/payments', wallet.inkey, {
out: false,
amount: amount,
memo: memo,
unit: unit,
lnurl_callback: lnurlCallback,
unit: unit,
fiat_provider: fiatProvider
}
if (internalMemo) {
data.extra = {
internal_memo: String(internalMemo)
}
}
return this.request('post', '/api/v1/payments', wallet.inkey, data)
})
},
payInvoice(wallet, bolt11, internalMemo = null) {
const data = {
payInvoice(wallet, bolt11) {
return this.request('post', '/api/v1/payments', wallet.adminkey, {
out: true,
bolt11: bolt11
}
if (internalMemo) {
data.extra = {
internal_memo: String(internalMemo)
}
}
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
})
},
payLnurl(
wallet,
@@ -57,28 +44,16 @@ window.LNbits = {
amount,
description = '',
comment = '',
unit = '',
internalMemo = null
unit = ''
) {
const data = {
return this.request('post', '/api/v1/payments/lnurl', wallet.adminkey, {
callback,
description_hash,
amount,
comment,
description,
unit
}
if (internalMemo) {
data.internal_memo = String(internalMemo)
}
return this.request(
'post',
'/api/v1/payments/lnurl',
wallet.adminkey,
data
)
})
},
authLnurl(wallet, callback) {
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
@@ -48,22 +48,6 @@ window.app.component('lnbits-funding-sources', {
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
}
],
[
'CLNRestWallet',
'Core Lightning Rest (plugin)',
{
clnrest_url: 'Endpoint',
clnrest_ca: 'ca.pem',
clnrest_cert: 'server.pem',
clnrest_readonly_rune: 'Rune used for readonly requests',
clnrest_invoice_rune: 'Rune used for creating invoices',
clnrest_pay_rune: 'Rune used for paying invoices using pay',
clnrest_renepay_rune: 'Rune used for paying invoices using renepay',
clnrest_last_pay_index:
'Ignores any invoices paid prior to or including this index. 0 is equivalent to not specifying and negative value is invalid.',
clnrest_nodeid: 'Node id'
}
],
[
'CoreLightningWallet',
'Core Lightning',
@@ -74,7 +58,7 @@ window.app.component('lnbits-funding-sources', {
],
[
'CoreLightningRestWallet',
'Core Lightning Rest (legacy)',
'Core Lightning Rest',
{
corelightning_rest_url: 'Endpoint',
corelightning_rest_cert: 'Certificate',
@@ -235,16 +219,6 @@ window.app.component('lnbits-funding-sources', {
strike_api_endpoint: 'API Endpoint',
strike_api_key: 'API Key'
}
],
[
'BreezLiquidSdkWallet',
'Breez Liquid SDK',
{
breez_liquid_api_key: 'Breez API Key (can be empty)',
breez_liquid_seed: 'Liquid seed phrase',
breez_liquid_fee_offset_sat:
'Offset amount in sats to increase fee limit'
}
]
]
}
+3 -17
View File
@@ -8,8 +8,7 @@ window.PaymentsPageLogic = {
searchData: {
wallet_id: null,
payment_hash: null,
memo: null,
internal_memo: null
memo: null
},
statusFilters: {
success: true,
@@ -61,10 +60,10 @@ window.PaymentsPageLogic = {
sortable: false
},
{
name: 'fee_sats',
name: 'fee',
align: 'left',
label: 'Fee',
field: 'fee_sats',
field: 'fee',
sortable: true
},
@@ -83,14 +82,6 @@ window.PaymentsPageLogic = {
sortable: false,
max_length: 20
},
{
name: 'internal_memo',
align: 'left',
label: 'Internal Memo',
field: 'internal_memo',
sortable: false,
max_length: 20
},
{
name: 'wallet_id',
align: 'left',
@@ -175,11 +166,6 @@ window.PaymentsPageLogic = {
p.extra.wallet_fiat_currency
)
}
if (p.extra?.internal_memo) {
p.internal_memo = p.extra.internal_memo
}
p.fee_sats =
new Intl.NumberFormat(window.LOCALE).format(p.fee / 1000) + ' sats'
return p
})
+4 -14
View File
@@ -14,7 +14,6 @@ window.WalletPageLogic = {
request: '',
amount: 0,
comment: '',
internalMemo: null,
unit: 'sat'
},
paymentChecker: null,
@@ -39,8 +38,7 @@ window.WalletPageLogic = {
fiatProvider: '',
data: {
amount: null,
memo: '',
internalMemo: null
memo: ''
}
},
invoiceQrCode: '',
@@ -199,7 +197,6 @@ window.WalletPageLogic = {
this.receive.paymentHash = null
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.data.internalMemo = null
this.receive.unit = this.isFiatPriority
? this.g.wallet.currency || 'sat'
: 'sat'
@@ -221,7 +218,6 @@ window.WalletPageLogic = {
window.isSecureContext && navigator.clipboard?.readText !== undefined
this.parse.data.request = ''
this.parse.data.comment = ''
this.parse.data.internalMemo = null
this.parse.data.paymentChecker = null
this.parse.camera.show = false
this.focusInput('textArea')
@@ -257,8 +253,7 @@ window.WalletPageLogic = {
this.receive.data.memo,
this.receive.unit,
this.receive.lnurl && this.receive.lnurl.callback,
this.receive.fiatProvider,
this.receive.data.internalMemo
this.receive.fiatProvider
)
.then(response => {
this.g.updatePayments = !this.g.updatePayments
@@ -482,11 +477,7 @@ window.WalletPageLogic = {
})
LNbits.api
.payInvoice(
this.g.wallet,
this.parse.data.request,
this.parse.data.internalMemo
)
.payInvoice(this.g.wallet, this.parse.data.request)
.then(response => {
dismissPaymentMsg()
this.updatePayments = !this.updatePayments
@@ -524,8 +515,7 @@ window.WalletPageLogic = {
this.parse.data.amount * 1000,
this.parse.lnurlpay.description.slice(0, 120),
this.parse.data.comment,
this.parse.data.unit,
this.parse.data.internalMemo
this.parse.data.unit
)
.then(response => {
this.parse.show = false
+12 -4
View File
@@ -18,16 +18,16 @@ $themes: (
'freedom': (
primary: #e22156,
secondary: #b91a45,
dark: #0a0a0a,
info: #1b1b1b,
dark: #462f36,
info: #47393d,
marginal-bg: #2d293b,
marginal-text: #fff
),
'cyber': (
primary: #7cb342,
secondary: #558b2f,
dark: #0a0a0a,
info: #1b1b1b,
dark: #000,
info: #1f2915,
marginal-bg: #2d293b,
marginal-text: #fff
),
@@ -62,5 +62,13 @@ $themes: (
info: rgb(39, 39, 39),
marginal-bg: #000,
marginal-text: rgb(255, 255, 255)
),
'salvador': (
primary: #1976d2,
secondary: #26a69a,
dark: #253647,
info: #343d47,
marginal-bg: #0d1620,
marginal-text: rgb(255, 255, 255)
)
);
+46 -11
View File
@@ -1,4 +1,5 @@
import asyncio
import time
import traceback
import uuid
from collections.abc import Coroutine
@@ -10,6 +11,7 @@ from typing import (
from loguru import logger
from lnbits.core.crud import (
get_payments,
get_standalone_payment,
update_payment,
)
@@ -151,18 +153,51 @@ def wait_for_paid_invoices(
return wrapper
def run_interval(
interval_seconds: int,
func: Callable[[], Coroutine],
) -> Callable[[], Coroutine]:
"""Run a function at a specified interval in seconds, while the server is running"""
async def check_pending_payments():
"""
check_pending_payments is called during startup to check for pending payments with
the backend and also to delete expired invoices. Incoming payments will be
checked only once, outgoing pending payments will be checked regularly.
"""
sleep_time = 60 * 30 # 30 minutes
async def wrapper() -> None:
while settings.lnbits_running:
await func()
await asyncio.sleep(interval_seconds)
return wrapper
while settings.lnbits_running:
funding_source = get_funding_source()
if funding_source.__class__.__name__ == "VoidWallet":
logger.warning("Task: skipping pending check for VoidWallet")
await asyncio.sleep(sleep_time)
continue
start_time = time.time()
pending_payments = await get_payments(
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
complete=False,
pending=True,
exclude_uncheckable=True,
)
count = len(pending_payments)
if count > 0:
logger.info(f"Task: checking {count} pending payments of last 15 days...")
for i, payment in enumerate(pending_payments):
status = await payment.check_status()
prefix = f"payment ({i+1} / {count})"
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment)
logger.debug(f"{prefix} failed {payment.checking_id}")
elif status.success:
payment.fee = status.fee_msat or 0
payment.preimage = status.preimage
payment.status = PaymentState.SUCCESS
await update_payment(payment)
logger.debug(f"{prefix} success {payment.checking_id}")
else:
logger.debug(f"{prefix} pending {payment.checking_id}")
await asyncio.sleep(0.01) # to avoid complete blocking
logger.info(
f"Task: pending check finished for {count} payments"
f" (took {time.time() - start_time:0.3f} s)"
)
await asyncio.sleep(sleep_time)
async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = False):
+2 -12
View File
@@ -880,24 +880,14 @@
:props="props"
style="white-space: normal; word-break: break-all"
>
<q-badge
v-if="props.row.tag"
color="yellow"
text-color="black"
class="q-mr-sm"
>
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a
v-text="'#' + props.row.tag"
class="inherit"
:href="['/', props.row.tag].join('')"
></a>
</q-badge>
<span v-text="props.row.memo"></span>
<span
class="text-grey-5 q-ml-sm ellipsis"
v-if="props.row.extra.internal_memo"
v-text="`(${props.row.extra.internal_memo})`"
></span>
<span class="q-ml-sm" v-text="props.row.memo"></span>
<br />
<i>
+1 -2
View File
@@ -66,8 +66,7 @@ class AESCipher:
self, salt: bytes, output_len: int = 32 + 16
) -> tuple[bytes, bytes]:
# extended from https://gist.github.com/gsakkis/4546068
if len(salt) != 8:
raise ValueError("Salt must be 8 bytes")
assert len(salt) == 8, "Salt must be 8 bytes"
data = self.key + salt
key = md5(data).digest()
final_key = key
+4 -8
View File
@@ -175,17 +175,14 @@ def normalize_private_key(key: str) -> str:
def normalize_bech32_key(hrp: str, key: str) -> str:
if key.startswith(hrp):
_, decoded_data = bech32_decode(key)
if not decoded_data:
raise ValueError(f"Key is not valid {hrp}.")
assert decoded_data, f"Key is not valid {hrp}."
decoded_data_bits = convertbits(decoded_data, 5, 8, False)
if not decoded_data_bits:
raise ValueError(f"Key is not valid {hrp}.")
assert decoded_data_bits, f"Key is not valid {hrp}."
return bytes(decoded_data_bits).hex()
if len(key) != 64:
raise ValueError("Key has wrong length.")
assert len(key) == 64, "Key has wrong length."
try:
int(key, 16)
except Exception as exc:
@@ -206,8 +203,7 @@ def hex_to_npub(hex_pubkey: str) -> str:
normalize_public_key(hex_pubkey)
pubkey_bytes = bytes.fromhex(hex_pubkey)
bits = convertbits(pubkey_bytes, 8, 5, True)
if not bits:
raise ValueError("Invalid Nostr public key.")
assert bits
return bech32_encode("npub", bits)
-1
View File
@@ -1 +0,0 @@
MIIBYzCCARWgAwIBAgIHPjlTc1IbAzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUwNzAyMDkxODMzWhcNMzUwNjMwMDkxODMzWjAfMQ8wDQYDVQQKEwZsbmJpdHMxDDAKBgNVBAMTA2RuaTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o38wfTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2jmj7l5rSw0yVb/vlWAYkK/YBwkwHwYDVR0jBBgwFoAU3qrWklbzjed0khb8TLYgsmsomGswHQYDVR0RBBYwFIESb2ZmaWNlQGRuaWxhYnMuY29tMAUGAytlcANBAMGS8jEfZbfNpv6mVrg328NXnjA/nG6TuGA0aAw0NyDz499aeu/0TURjF8FzmxzmdNOiffUZ6akPWCZUKFYuGgA=
-4
View File
@@ -10,9 +10,7 @@ from .alby import AlbyWallet
from .blink import BlinkWallet
from .boltz import BoltzWallet
from .breez import BreezSdkWallet
from .breez_liquid import BreezLiquidSdkWallet
from .cliche import ClicheWallet
from .clnrest import CLNRestWallet
from .corelightning import CoreLightningWallet
# The following import is intentional to keep backwards compatibility
@@ -59,9 +57,7 @@ __all__ = [
"AlbyWallet",
"BlinkWallet",
"BoltzWallet",
"BreezLiquidSdkWallet",
"BreezSdkWallet",
"CLNRestWallet",
"CLightningWallet",
"ClicheWallet",
"CoreLightningRestWallet",
+1 -4
View File
@@ -7,8 +7,6 @@ from typing import TYPE_CHECKING, NamedTuple
from loguru import logger
from lnbits.settings import settings
if TYPE_CHECKING:
from lnbits.nodes.base import Node
@@ -24,7 +22,6 @@ class InvoiceResponse(NamedTuple):
payment_request: str | None = None
error_message: str | None = None
preimage: str | None = None
fee_msat: int | None = None
@property
def success(self) -> bool:
@@ -142,7 +139,7 @@ class Wallet(ABC):
pass
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
while True:
for invoice in self.pending_invoices:
try:
status = await self.get_invoice_status(invoice)
+7 -8
View File
@@ -7,7 +7,7 @@ from typing import Optional
import httpx
from loguru import logger
from pydantic import BaseModel
from websockets.legacy.client import connect
from websockets.legacy.client import WebSocketClientProtocol, connect
from websockets.typing import Subprotocol
from lnbits import bolt11
@@ -48,7 +48,7 @@ class BlinkWallet(Wallet):
"payload": {"X-API-KEY": settings.blink_token},
}
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
self.ws = None
self.ws: Optional[WebSocketClientProtocol] = None
self._wallet_id = None
@property
@@ -252,8 +252,7 @@ class BlinkWallet(Wallet):
response = await self._graphql_query(data)
response_data = response.get("data")
if response_data is None:
raise ValueError("No data found in response.")
assert response_data is not None
txs_data = (
response_data.get("me", {})
.get("defaultAccount", {})
@@ -261,8 +260,7 @@ class BlinkWallet(Wallet):
.get("transactionsByPaymentHash", [])
)
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
if not tx_data:
raise ValueError("No SEND data found.")
assert tx_data, "No SEND data found."
fee = tx_data.get("settlementFee")
preimage = tx_data.get("settlementVia", {}).get("preImage")
status = tx_data.get("status")
@@ -286,8 +284,9 @@ class BlinkWallet(Wallet):
await ws.send(json.dumps(self.ws_auth))
confirmation = await ws.recv()
ack = json.loads(confirmation)
if ack.get("type") != "connection_ack":
raise ValueError("Websocket connection not acknowledged.")
assert (
ack.get("type") == "connection_ack"
), "Websocket connection not acknowledged."
logger.info("Websocket connection acknowledged.")
subscription_req = {
+3 -4
View File
@@ -63,7 +63,7 @@ class BoltzWallet(Wallet):
channel = grpc.aio.insecure_channel(settings.boltz_client_endpoint)
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
self.wallet_id = 0
self.wallet_id: int = 0
# Auto-create wallet if running in Docker mode
async def _init_boltz_wallet():
@@ -142,8 +142,7 @@ class BoltzWallet(Wallet):
pair_info = await self.rpc.GetPairInfo(pair_request, metadata=self.metadata)
invoice = decode(bolt11)
if not invoice.amount_msat:
raise ValueError("amountless invoice")
assert invoice.amount_msat, "amountless invoice"
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
estimate = service_fee + pair_info.fees.miner_fees * 1000
if estimate > fee_limit_msat:
@@ -237,7 +236,7 @@ class BoltzWallet(Wallet):
return PaymentFailedStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
while True:
try:
request = boltzrpc_pb2.GetSwapInfoRequest()
info: boltzrpc_pb2.GetSwapInfoResponse
+74 -100
View File
@@ -1,9 +1,15 @@
import base64
from importlib.util import find_spec
from lnbits.exceptions import UnsupportedError
if not find_spec("breez_sdk"):
try:
import breez_sdk # type: ignore
BREEZ_SDK_INSTALLED = True
except ImportError:
BREEZ_SDK_INSTALLED = False
if not BREEZ_SDK_INSTALLED:
class BreezSdkWallet: # pyright: ignore
def __init__(self):
@@ -18,31 +24,9 @@ else:
from pathlib import Path
from typing import Optional
from bolt11 import Bolt11Exception
from bolt11 import decode as bolt11_decode
from breez_sdk import (
BreezEvent,
ConnectRequest,
EnvironmentType,
EventListener,
GreenlightCredentials,
GreenlightNodeConfig,
NodeConfig,
PaymentDetails,
PaymentType,
ReceivePaymentRequest,
ReceivePaymentResponse,
ReportIssueRequest,
ReportPaymentFailureDetails,
SendPaymentRequest,
SendPaymentResponse,
connect,
default_config,
mnemonic_to_seed,
)
from breez_sdk import PaymentStatus as BreezPaymentStatus
from loguru import logger
from lnbits import bolt11 as lnbits_bolt11
from lnbits.settings import settings
from .base import (
@@ -56,15 +40,7 @@ else:
Wallet,
)
breez_incoming_queue: asyncio.Queue[PaymentDetails.LN] = asyncio.Queue()
class PaymentsListener(EventListener):
def on_event(self, e: BreezEvent) -> None:
logger.debug(f"received breez sdk event: {e}")
if isinstance(e, BreezEvent.PAYMENT_SUCCEED) and isinstance(
e.details, PaymentDetails.LN
):
breez_incoming_queue.put_nowait(e.details)
breez_event_queue: asyncio.Queue = asyncio.Queue()
def load_bytes(source: str, extension: str) -> Optional[bytes]:
# first check if it can be read from a file
@@ -81,11 +57,15 @@ else:
# else convert from base64
try:
return base64.b64decode(source)
except Exception as exc:
logger.debug(exc)
except Exception:
pass
return None
def load_greenlight_credentials() -> Optional[GreenlightCredentials]:
def load_greenlight_credentials() -> (
Optional[
breez_sdk.GreenlightCredentials # pyright: ignore[reportUnboundVariable]
]
):
if (
settings.breez_greenlight_device_key
and settings.breez_greenlight_device_cert
@@ -98,12 +78,19 @@ else:
"cannot decode breez_greenlight_device_key "
"or breez_greenlight_device_cert"
)
return GreenlightCredentials(
return breez_sdk.GreenlightCredentials( # pyright: ignore[reportUnboundVariable]
developer_key=list(device_key_bytes),
developer_cert=list(device_cert_bytes),
)
return None
class SDKListener(
breez_sdk.EventListener # pyright: ignore[reportUnboundVariable]
):
def on_event(self, event):
logger.debug(event)
breez_event_queue.put_nowait(event)
class BreezSdkWallet(Wallet): # type: ignore[no-redef]
def __init__(self):
if not settings.breez_greenlight_seed:
@@ -131,15 +118,15 @@ else:
"missing breez_greenlight_device_key"
)
gl_config = GreenlightNodeConfig(
partner_credentials=load_greenlight_credentials(),
invite_code=settings.breez_greenlight_invite_code,
)
node_config = NodeConfig.GREENLIGHT(config=gl_config)
self.config = default_config(
EnvironmentType.PRODUCTION,
self.config = breez_sdk.default_config(
breez_sdk.EnvironmentType.PRODUCTION,
settings.breez_api_key,
node_config=node_config, # type: ignore[arg-type]
breez_sdk.NodeConfig.GREENLIGHT(
config=breez_sdk.GreenlightNodeConfig(
partner_credentials=load_greenlight_credentials(),
invite_code=settings.breez_greenlight_invite_code,
)
),
)
breez_sdk_working_dir = Path(settings.lnbits_data_folder, "breez-sdk")
@@ -147,9 +134,9 @@ else:
self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
try:
seed = mnemonic_to_seed(settings.breez_greenlight_seed)
connect_request = ConnectRequest(config=self.config, seed=seed)
self.sdk_services = connect(connect_request, PaymentsListener())
seed = breez_sdk.mnemonic_to_seed(settings.breez_greenlight_seed)
connect_request = breez_sdk.ConnectRequest(self.config, seed)
self.sdk_services = breez_sdk.connect(connect_request, SDKListener())
except Exception as exc:
logger.warning(exc)
raise ValueError(f"cannot initialize BreezSdkWallet: {exc!s}") from exc
@@ -159,7 +146,7 @@ else:
async def status(self) -> StatusResponse:
try:
node_info = self.sdk_services.node_info()
node_info: breez_sdk.NodeState = self.sdk_services.node_info()
except Exception as exc:
return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
@@ -181,14 +168,14 @@ else:
"'description_hash' unsupported by Greenlight, provide"
" 'unhashed_description'"
)
breez_invoice: ReceivePaymentResponse = (
breez_invoice: breez_sdk.ReceivePaymentResponse = (
self.sdk_services.receive_payment(
ReceivePaymentRequest(
amount_msat=amount * 1000, # breez uses msat
description=(
breez_sdk.ReceivePaymentRequest(
amount * 1000, # breez uses msat
(
unhashed_description.decode()
if unhashed_description
else memo or ""
else memo
),
preimage=kwargs.get("preimage"),
opening_fee_params=None,
@@ -211,45 +198,36 @@ else:
async def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> PaymentResponse:
logger.debug(f"fee_limit_msat {fee_limit_msat} is ignored by Breez SDK")
invoice = lnbits_bolt11.decode(bolt11)
try:
invoice = bolt11_decode(bolt11)
except Bolt11Exception as exc:
logger.warning(exc)
return PaymentResponse(
ok=False, error_message=f"invalid bolt11 invoice: {exc}"
)
try:
send_payment_request = SendPaymentRequest(
send_payment_request = breez_sdk.SendPaymentRequest(
bolt11=bolt11, use_trampoline=settings.breez_use_trampoline
)
send_payment_response: SendPaymentResponse = (
send_payment_response: breez_sdk.SendPaymentResponse = (
self.sdk_services.send_payment(send_payment_request)
)
payment = send_payment_response.payment
payment: breez_sdk.Payment = send_payment_response.payment
except Exception as exc:
logger.warning(exc)
try:
# report issue to Breez to improve LSP routing
payment_error = ReportIssueRequest.PAYMENT_FAILURE(
ReportPaymentFailureDetails(payment_hash=invoice.payment_hash)
# try to report issue to Breez to improve LSP routing
self.sdk_services.report_issue(
breez_sdk.ReportIssueRequest.PAYMENT_FAILURE(
breez_sdk.ReportPaymentFailureDetails(invoice.payment_hash)
)
)
self.sdk_services.report_issue(payment_error) # type: ignore[arg-type]
except Exception as ex:
logger.info(ex)
return PaymentResponse(error_message=f"exception while payment {exc!s}")
# assume that payment failed?
return PaymentResponse(ok=False, error_message=f"payment failed: {exc}")
if payment.status != BreezPaymentStatus.COMPLETE:
return PaymentResponse(ok=None, error_message="payment is pending")
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
return PaymentResponse(ok=False, error_message="payment is pending")
# let's use the payment_hash as the checking_id
checking_id = invoice.payment_hash
if not isinstance(payment.details, PaymentDetails.LN):
return PaymentResponse(
error_message="Breez SDK returned a non-LN payment details object",
)
return PaymentResponse(
ok=True,
checking_id=checking_id,
@@ -259,22 +237,18 @@ else:
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
payment = self.sdk_services.payment_by_hash(checking_id)
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
checking_id
)
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != PaymentType.RECEIVED:
if payment.payment_type != breez_sdk.PaymentType.RECEIVED:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if not isinstance(payment.details, PaymentDetails.LN):
logger.warning(f"unexpected paymentdetails type: {payment.details}")
return PaymentPendingStatus()
if payment.status == BreezPaymentStatus.FAILED:
if payment.status == breez_sdk.PaymentStatus.FAILED:
return PaymentFailedStatus()
if payment.status == BreezPaymentStatus.COMPLETE:
return PaymentSuccessStatus(
fee_msat=payment.fee_msat,
preimage=payment.details.data.payment_preimage,
)
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
return PaymentSuccessStatus()
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
@@ -282,21 +256,20 @@ else:
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
payment = self.sdk_services.payment_by_hash(checking_id)
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
checking_id
)
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != PaymentType.SENT:
logger.warning(f"unexpected payment type: {payment.payment_type}")
if payment.payment_type != breez_sdk.PaymentType.SENT:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if not isinstance(payment.details, PaymentDetails.LN):
logger.warning(f"unexpected paymentdetails type: {payment.details}")
return PaymentPendingStatus()
if payment.status == BreezPaymentStatus.COMPLETE:
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
return PaymentSuccessStatus(
fee_msat=payment.fee_msat,
preimage=payment.details.data.payment_preimage,
)
if payment.status == BreezPaymentStatus.FAILED:
if payment.status == breez_sdk.PaymentStatus.FAILED:
return PaymentFailedStatus()
return PaymentPendingStatus()
except Exception as exc:
@@ -304,6 +277,7 @@ else:
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
details = await breez_incoming_queue.get()
yield details.data.payment_hash
while True:
event = await breez_event_queue.get()
if event.is_invoice_paid():
yield event.details.payment_hash
-311
View File
@@ -1,311 +0,0 @@
# Based on breez.py
from importlib.util import find_spec
if not find_spec("breez_sdk_liquid"):
class BreezLiquidSdkWallet: # pyright: ignore
def __init__(self):
raise RuntimeError(
"Breez Liquid SDK is not installed. "
"Ask admin to run `poetry add -E breez` to install it."
)
else:
import asyncio
from asyncio import Queue
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Optional
from bolt11 import decode as bolt11_decode
from breez_sdk_liquid import (
ConnectRequest,
EventListener,
GetInfoResponse,
GetPaymentRequest,
LiquidNetwork,
Payment,
PaymentDetails,
PaymentMethod,
PaymentState,
PaymentType,
PrepareReceiveRequest,
PrepareSendRequest,
ReceiveAmount,
ReceivePaymentRequest,
SdkEvent,
SendPaymentRequest,
connect,
default_config,
)
from loguru import logger
from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
Wallet,
)
breez_incoming_queue: Queue[PaymentDetails.LIGHTNING] = Queue()
breez_outgoing_queue: dict[str, Queue[PaymentDetails.LIGHTNING]] = {}
class PaymentsListener(EventListener):
def on_event(self, e: SdkEvent) -> None:
logger.debug(f"received breez sdk event: {e}")
# TODO: when this issue is fixed:
# https://github.com/breez/breez-sdk-liquid/issues/961
# use SdkEvent.PAYMENT_WAITING_CONFIRMATION
if not isinstance(e, SdkEvent.PAYMENT_SUCCEEDED) or not isinstance(
e.details.details, PaymentDetails.LIGHTNING
):
return
payment = e.details
payment_details = e.details.details
if payment.payment_type is PaymentType.RECEIVE:
breez_incoming_queue.put_nowait(payment_details)
elif (
payment.payment_type is PaymentType.SEND
and payment_details.payment_hash in breez_outgoing_queue
):
breez_outgoing_queue[payment_details.payment_hash].put_nowait(
payment_details
)
class BreezLiquidSdkWallet(Wallet): # type: ignore[no-redef]
def __init__(self):
if not settings.breez_liquid_seed:
raise ValueError(
"cannot initialize BreezLiquidSdkWallet: missing breez_liquid_seed"
)
if not settings.breez_liquid_api_key:
with open(Path("lnbits/wallets", ".breez")) as f:
settings.breez_liquid_api_key = f.read().strip()
self.config = default_config(
LiquidNetwork.MAINNET,
breez_api_key=settings.breez_liquid_api_key,
)
breez_sdk_working_dir = Path(
settings.lnbits_data_folder, "breez-liquid-sdk"
)
breez_sdk_working_dir.mkdir(parents=True, exist_ok=True)
self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
try:
mnemonic = settings.breez_liquid_seed
connect_request = ConnectRequest(config=self.config, mnemonic=mnemonic)
self.sdk_services = connect(connect_request)
self.sdk_services.add_event_listener(PaymentsListener())
except Exception as exc:
logger.warning(exc)
raise ValueError(
f"cannot initialize BreezLiquidSdkWallet: {exc!s}"
) from exc
async def cleanup(self):
self.sdk_services.disconnect()
async def status(self) -> StatusResponse:
try:
info: GetInfoResponse = self.sdk_services.get_info()
except Exception as exc:
logger.warning(exc)
return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
return StatusResponse(None, int(info.wallet_info.balance_sat * 1000))
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**_,
) -> InvoiceResponse:
try:
# issue with breez sdk, receive_amount is of type BITCOIN
# not ReceiveAmount after initialisation
receive_amount = ReceiveAmount.BITCOIN(amount)
req = self.sdk_services.prepare_receive_payment(
PrepareReceiveRequest(
payment_method=PaymentMethod.BOLT11_INVOICE,
amount=receive_amount, # type: ignore
)
)
receive_fees_sats = req.fees_sat
description = memo or (
unhashed_description.decode() if unhashed_description else ""
)
res = self.sdk_services.receive_payment(
ReceivePaymentRequest(
prepare_response=req,
description=description,
use_description_hash=description_hash is not None,
)
)
bolt11 = res.destination
invoice_data = bolt11_decode(bolt11)
payment_hash = invoice_data.payment_hash
return InvoiceResponse(
ok=True,
checking_id=payment_hash,
payment_request=bolt11,
fee_msat=receive_fees_sats * 1000,
)
except Exception as e:
logger.warning(e)
return InvoiceResponse(ok=False, error_message=str(e))
async def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> PaymentResponse:
invoice_data = bolt11_decode(bolt11)
try:
prepare_req = PrepareSendRequest(destination=bolt11)
req = self.sdk_services.prepare_send_payment(prepare_req)
fee_limit_sat = settings.breez_liquid_fee_offset_sat + int(
fee_limit_msat / 1000
)
if req.fees_sat and req.fees_sat > fee_limit_sat:
return PaymentResponse(
ok=False,
error_message=(
f"fee of {req.fees_sat} sat exceeds limit of "
f"{fee_limit_sat} sat"
),
)
send_response = self.sdk_services.send_payment(
SendPaymentRequest(prepare_response=req)
)
except Exception as exc:
logger.warning(exc)
return PaymentResponse(error_message=f"Exception while payment: {exc}")
payment: Payment = send_response.payment
logger.debug(f"pay invoice res: {payment}")
checking_id = invoice_data.payment_hash
fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0
if payment.status != PaymentState.COMPLETE:
return await self._wait_for_outgoing_payment(checking_id, fees, 10)
if not isinstance(payment.details, PaymentDetails.LIGHTNING):
return PaymentResponse(
error_message="lightning payment details are not available"
)
return PaymentResponse(
ok=True,
checking_id=checking_id,
fee_msat=payment.fees_sat * 1000,
preimage=payment.details.preimage,
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
req = GetPaymentRequest.PAYMENT_HASH(checking_id)
payment = self.sdk_services.get_payment(req=req) # type: ignore
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != PaymentType.RECEIVE:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if payment.status == PaymentState.FAILED:
return PaymentFailedStatus()
if payment.status == PaymentState.COMPLETE and isinstance(
payment.details, PaymentDetails.LIGHTNING
):
return PaymentSuccessStatus(
paid=True,
fee_msat=int(payment.fees_sat * 1000),
preimage=payment.details.preimage,
)
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
req = GetPaymentRequest.PAYMENT_HASH(checking_id)
payment = self.sdk_services.get_payment(req=req) # type: ignore
if payment is None:
return PaymentPendingStatus()
if payment.payment_type != PaymentType.SEND:
logger.warning(f"unexpected payment type: {payment.status}")
return PaymentPendingStatus()
if payment.status == PaymentState.COMPLETE:
if not isinstance(payment.details, PaymentDetails.LIGHTNING):
logger.warning("payment details are not of type LIGHTNING")
return PaymentPendingStatus()
return PaymentSuccessStatus(
fee_msat=int(payment.fees_sat * 1000),
preimage=payment.details.preimage,
)
if payment.status == PaymentState.FAILED:
return PaymentFailedStatus()
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
details = await breez_incoming_queue.get()
logger.debug(f"breez invoice paid event: {details}")
if not details.invoice:
logger.warning(
"Paid invoices stream expected bolt11 invoice, got None"
)
continue
invoice_data = bolt11_decode(details.invoice)
yield invoice_data.payment_hash
async def _wait_for_outgoing_payment(
self, checking_id: str, fees: int, timeout: int
) -> PaymentResponse:
logger.debug(f"waiting for outgoing payment {checking_id} to complete")
try:
breez_outgoing_queue[checking_id] = Queue()
payment_details = await asyncio.wait_for(
breez_outgoing_queue[checking_id].get(), timeout
)
return PaymentResponse(
ok=True,
preimage=payment_details.preimage,
checking_id=checking_id,
fee_msat=fees,
)
except asyncio.TimeoutError:
logger.debug(
f"payment '{checking_id}' is still pending after {timeout} seconds"
)
return PaymentResponse(
checking_id=checking_id,
fee_msat=fees,
error_message="payment is pending",
)
finally:
breez_outgoing_queue.pop(checking_id, None)
+1 -2
View File
@@ -182,8 +182,7 @@ class ClicheWallet(Wallet):
try:
if data["result"]["status"]:
yield data["result"]["payment_hash"]
except Exception as exc:
logger.debug(exc)
except Exception:
continue
except Exception as exc:
logger.error(
-498
View File
@@ -1,498 +0,0 @@
import asyncio
import base64
import json
import os
import ssl
import uuid
from collections.abc import AsyncGenerator
from typing import Optional
from urllib.parse import urlparse
import httpx
from bolt11 import Bolt11Exception
from bolt11.decode import decode
from loguru import logger
from lnbits.exceptions import UnsupportedError
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
InvoiceResponse,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
Wallet,
)
class CLNRestWallet(Wallet):
def __init__(self):
if not settings.clnrest_url:
raise ValueError("Cannot initialize CLNRestWallet: missing CLNREST_URL")
if not settings.clnrest_readonly_rune:
raise ValueError(
"cannot initialize CLNRestWallet: " "missing clnrest_readonly_rune"
)
self.url = normalize_endpoint(settings.clnrest_url)
if not settings.clnrest_nodeid:
logger.info("missing CLNREST_NODEID, but this is only needed for v23.08")
self.base_headers = {
"accept": "application/json",
"User-Agent": settings.user_agent,
"Content-Type": "application/json",
}
if settings.clnrest_nodeid is not None:
self.base_headers["nodeid"] = settings.clnrest_nodeid
# Ensure the readonly rune is set
if settings.clnrest_readonly_rune is not None:
self.readonly_headers = {
**self.base_headers,
"rune": settings.clnrest_readonly_rune,
}
else:
logger.warning(
"Readonly rune 'CLNREST_READONLY_RUNE' is required but not set."
)
if settings.clnrest_invoice_rune is not None:
self.invoice_headers = {
**self.base_headers,
"rune": settings.clnrest_invoice_rune,
}
else:
logger.warning(
"Will be unable to create any invoices without "
"setting 'CLNREST_INVOICE_RUNE[:4]'"
)
if settings.clnrest_pay_rune is not None:
self.pay_headers = {**self.base_headers, "rune": settings.clnrest_pay_rune}
else:
logger.warning(
"Will be unable to call pay endpoint without setting 'CLNREST_PAY_RUNE'"
)
if settings.clnrest_renepay_rune is not None:
self.renepay_headers = {
**self.base_headers,
"rune": settings.clnrest_renepay_rune,
}
else:
logger.warning(
"Will be unable to call renepay endpoint without "
"setting 'CLNREST_RENEPAY_RUNE'"
)
# https://docs.corelightning.org/reference/lightning-pay
# -32602: Invalid bolt11: Prefix bc is not for regtest
# -1: Catchall nonspecific error.
## 201: Already paid
# 203: Permanent failure at destination.
# 205: Unable to find a route.
# 206: Route too expensive.
# 207: Invoice expired.
# 210: Payment timed out without a payment in progress.
# 401: Unauthorized. Probably a rune issue
self.pay_failure_error_codes = [-32602, 203, 205, 206, 207, 210, 401]
self.client = self._create_client()
self.last_pay_index = settings.clnrest_last_pay_index
self.statuses = {
"paid": True,
"complete": True,
"failed": False,
"pending": None,
}
async def cleanup(self):
try:
await self.client.aclose()
except RuntimeError as exc:
logger.warning(f"Error closing wallet connection: {exc}")
async def status(self) -> StatusResponse:
try:
logger.debug("REQUEST to /v1/listfunds")
r = await self.client.post(
"/v1/listfunds", timeout=15, headers=self.readonly_headers
)
r.raise_for_status()
response_data = r.json()
if not response_data:
logger.warning("Received empty response data")
return StatusResponse("no data", 0)
channels = response_data.get("channels", [])
total_our_amount_msat = sum(
channel.get("our_amount_msat", 0) for channel in channels
)
return StatusResponse(None, total_our_amount_msat)
except json.JSONDecodeError as exc:
logger.warning(f"JSON decode error: {exc!s}")
return StatusResponse(f"Failed to decode JSON response from {self.url}", 0)
except httpx.ReadTimeout:
logger.warning(
"Timeout error: The server did not respond in time. This can happen if "
"the server is running HTTPS but the client is using HTTP."
)
return StatusResponse(
"Unable to connect to 'v1/listfunds' due to timeout", 0
)
except (httpx.ConnectError, httpx.RequestError) as exc:
logger.warning(f"Connection error: {exc}")
return StatusResponse("Unable to connect to 'v1/listfunds'", 0)
except httpx.HTTPStatusError as exc:
logger.warning(
f"HTTP error: {exc.response.status_code} {exc.response.reason_phrase} "
f"while accessing {exc.request.url}"
)
return StatusResponse(
f"Failed with HTTP {exc.response.status_code} on 'v1/listfunds'", 0
)
except Exception as exc:
logger.warning(exc)
return StatusResponse(f"Unable to connect to {self.url}.", 0)
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
if not settings.clnrest_invoice_rune:
return InvoiceResponse(
ok=False, error_message="Unable to invoice without an invoice rune"
)
data: dict = {
"amount_msat": int(amount * 1000),
"description": memo,
"label": _generate_label(),
}
if description_hash and not unhashed_description:
raise UnsupportedError(
"'description_hash' unsupported by CoreLightningRest, "
"provide 'unhashed_description'"
)
if unhashed_description:
data["description"] = unhashed_description.decode()
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if kwargs.get("preimage"):
data["preimage"] = kwargs["preimage"]
else:
preimage, _ = random_secret_and_hash()
data["preimage"] = preimage
try:
r = await self.client.post(
"/v1/invoice",
json=data,
headers=self.invoice_headers,
)
r.raise_for_status()
response_data = r.json()
if "error" in response_data:
error_message = response_data["error"]
logger.debug(f"Error creating invoice: {error_message}")
return InvoiceResponse(ok=False, error_message=error_message)
if "payment_hash" not in response_data or "bolt11" not in response_data:
return InvoiceResponse(
ok=False, error_message="Server error: 'missing required fields'"
)
return InvoiceResponse(
ok=True,
checking_id=response_data["payment_hash"],
payment_request=response_data["bolt11"],
preimage=data["preimage"],
)
except json.JSONDecodeError as exc:
logger.warning(exc)
return InvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(f"Unable to connect to {self.url}: {exc}")
return InvoiceResponse(
ok=False, error_message=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))
if not invoice.amount_msat or invoice.amount_msat <= 0:
return PaymentResponse(
ok=False, error_message="0 amount invoices are not allowed"
)
if not settings.clnrest_pay_rune and not settings.clnrest_renepay_rune:
return PaymentResponse(
ok=False,
error_message="Unable to pay invoice without a pay or renepay rune",
)
data = {
"label": _generate_label(),
"description": invoice.description,
"maxfee": fee_limit_msat,
}
if settings.clnrest_renepay_rune:
endpoint = "/v1/renepay"
headers = self.renepay_headers
data["invstring"] = bolt11
else:
endpoint = "/v1/pay"
headers = self.pay_headers
data["bolt11"] = bolt11
try:
r = await self.client.post(
endpoint,
json=data,
headers=headers,
timeout=None,
)
r.raise_for_status()
data = r.json()
if "payment_preimage" not in data:
error_message = data.get("error", "No payment preimage in response")
logger.warning(error_message)
return PaymentResponse(error_message=error_message)
return PaymentResponse(
ok=self.statuses.get(data["status"]),
checking_id=data["payment_hash"],
fee_msat=data["amount_sent_msat"] - data["amount_msat"],
preimage=data["payment_preimage"],
)
except httpx.HTTPStatusError as exc:
try:
data = exc.response.json()
error = data.get("error", {})
error_code = int(error.get("code", 0))
error_message = error.get("message", "Unknown error")
if error_code in self.pay_failure_error_codes:
return PaymentResponse(ok=False, error_message=error_message)
else:
return PaymentResponse(error_message=error_message)
except Exception:
error_message = f"Error parsing response from {self.url}: {exc!s}"
logger.warning(error_message)
return PaymentResponse(error_message=error_message)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11}")
logger.warning(exc)
error_message = f"Unable to connect to {self.url}."
return PaymentResponse(error_message=error_message)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
data: dict = {"payment_hash": checking_id}
try:
r = await self.client.post(
"/v1/listinvoices",
json=data,
headers=self.readonly_headers,
)
r.raise_for_status()
data = r.json()
if r.is_error or "error" in data or data.get("invoices") is None:
logger.warning(f"error in cln response '{checking_id}'")
return PaymentPendingStatus()
status = self.statuses.get(data["invoices"][0]["status"])
fee_msat = data["invoices"][0].get("amount_received_msat", 0) - data[
"invoices"
][0].get("amount_msat", 0)
return PaymentStatus(
paid=status,
preimage=data["invoices"][0].get("preimage"),
fee_msat=fee_msat,
)
except Exception as exc:
logger.warning(f"Error getting invoice status: {exc}")
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
data: dict = {"payment_hash": checking_id}
r = await self.client.post(
"/v1/listpays",
json=data,
headers=self.readonly_headers,
)
r.raise_for_status()
data = r.json()
if r.is_error or "error" in data:
logger.warning(f"API response error: {data}")
return PaymentPendingStatus()
pays_list = data.get("pays", [])
if not pays_list:
logger.warning(f"No payments found for payment hash {checking_id}.")
return PaymentPendingStatus()
if len(pays_list) != 1:
logger.warning(
f"Expected one payment status, but found {len(pays_list)}"
)
return PaymentPendingStatus()
pay = pays_list[-1]
if pay["status"] == "complete":
fee_msat = pay["amount_sent_msat"] - pay["amount_msat"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=pay["preimage"])
except Exception as exc:
logger.warning(f"Error getting payment status: {exc}")
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
try:
waitanyinvoice_timeout = None
request_timeout = httpx.Timeout(
connect=5.0, read=waitanyinvoice_timeout, write=60.0, pool=60.0
)
data: dict = {
"lastpay_index": self.last_pay_index,
"timeout": waitanyinvoice_timeout,
}
url = "/v1/waitanyinvoice"
async with self.client.stream(
"POST",
url,
json=data,
headers=self.readonly_headers,
timeout=request_timeout,
) as r:
async for line in r.aiter_lines():
inv = json.loads(line)
if "error" in inv and "message" in inv["error"]:
logger.warning("Error in paid_invoices_stream:", inv)
raise Exception(inv["error"]["message"])
try:
paid = inv["status"] == "paid"
self.last_pay_index = inv["pay_index"]
if not paid:
continue
except Exception as exc:
logger.warning(exc)
continue
logger.debug(f"paid invoice: {inv}")
if "payment_hash" in inv:
payment_hash_from_waitanyinvoice = inv["payment_hash"]
yield payment_hash_from_waitanyinvoice
except Exception as exc:
logger.debug(
f"lost connection to corelightning-rest invoices stream: '{exc}', "
"reconnecting..."
)
await asyncio.sleep(0.02)
def _create_client(self) -> httpx.AsyncClient:
"""Create an HTTP client with specified headers and SSL configuration."""
parsed_url = urlparse(self.url)
# Validate the URL scheme
if parsed_url.scheme == "http":
if parsed_url.hostname in ("localhost", "127.0.0.1", "::1"):
logger.warning("Not using SSL for connection to CLNRestWallet")
else:
raise ValueError(
"Insecure HTTP connections are only allowed for localhost or "
"equivalent IP addresses. Set CLNREST_URL to https:// for external "
"connections or use localhost."
)
return httpx.AsyncClient(base_url=self.url)
if parsed_url.scheme == "https":
logger.info(f"Using SSL to connect to {self.url}")
# Check for CA certificate
if not settings.clnrest_ca:
logger.warning(
"No CA certificate provided for CLNRestWallet. "
"This setup requires a CA certificate for server authentication "
"and trust. Set CLNREST_CA to the CA certificate file path or the "
"contents of the certificate."
)
raise ValueError("CA certificate is required for secure communication.")
logger.info(f"CA Certificate provided: {settings.clnrest_ca}")
# Create an SSL context and load the CA certificate
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
# Load CA certificate
if os.path.isfile(settings.clnrest_ca):
logger.info(f"Using CA certificate file: {settings.clnrest_ca}")
ssl_context.load_verify_locations(cafile=settings.clnrest_ca)
else:
logger.info("Using CA certificate from PEM string: ")
ca_content = settings.clnrest_ca.replace("\\n", "\n")
logger.info(ca_content)
ssl_context.load_verify_locations(cadata=ca_content)
# Optional: Disable hostname checking if necessary
# especially for ip addresses
ssl_context.check_hostname = False
# Create the HTTP client without a client certificate
client = httpx.AsyncClient(base_url=self.url, verify=ssl_context)
return client
else:
raise ValueError("CLNREST_URL must start with http:// or https://")
def _generate_label() -> str:
"""Generate a unique label for the invoice."""
random_uuid = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip(b"=").decode()
return f"LNbits_{random_uuid}"
+2 -2
View File
@@ -1,6 +1,6 @@
import asyncio
import random
from collections.abc import AsyncGenerator
from secrets import token_urlsafe
from typing import Any, Optional
from bolt11.decode import decode as bolt11_decode
@@ -92,7 +92,7 @@ class CoreLightningWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
label = kwargs.get("label", f"lbl{random.random()}")
msat: int = int(amount * 1000)
try:
if description_hash and not unhashed_description:
+8 -10
View File
@@ -1,7 +1,7 @@
import asyncio
import json
import random
from collections.abc import AsyncGenerator
from secrets import token_urlsafe
from typing import Optional
import httpx
@@ -111,7 +111,7 @@ class CoreLightningRestWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
label = kwargs.get("label", f"lbl{random.random()}")
data: dict = {
"amount": amount * 1000,
"description": memo,
@@ -298,8 +298,7 @@ class CoreLightningRestWallet(Wallet):
self.last_pay_index = inv["pay_index"]
if not paid:
continue
except Exception as exc:
logger.debug(exc)
except Exception:
continue
logger.trace(f"paid invoice: {inv}")
@@ -314,12 +313,11 @@ class CoreLightningRestWallet(Wallet):
)
paid_invoice = r.json()
logger.trace(f"paid invoice: {paid_invoice}")
if not self.statuses[paid_invoice["invoices"][0]["status"]]:
raise ValueError("streamed invoice not paid")
if "invoices" not in paid_invoice:
raise ValueError("no invoices in response")
if not len(paid_invoice["invoices"]):
raise ValueError("no invoices in response")
assert self.statuses[
paid_invoice["invoices"][0]["status"]
], "streamed invoice not paid"
assert "invoices" in paid_invoice, "no invoices in response"
assert len(paid_invoice["invoices"]), "no invoices in response"
yield paid_invoice["invoices"][0]["payment_hash"]
except Exception as exc:
+2 -4
View File
@@ -292,8 +292,7 @@ class LndRestWallet(Wallet):
)
else:
return PaymentPendingStatus()
except Exception as exc:
logger.debug(exc)
except Exception:
continue
return PaymentPendingStatus()
@@ -308,8 +307,7 @@ class LndRestWallet(Wallet):
inv = json.loads(line)["result"]
if not inv["settled"]:
continue
except Exception as exc:
logger.debug(exc)
except Exception:
continue
payment_hash = base64.b64decode(inv["r_hash"]).hex()
+3 -4
View File
@@ -173,12 +173,11 @@ class LnTipsWallet(Wallet):
inv = json.loads(data)
if not inv.get("payment_hash"):
continue
except Exception as exc:
logger.debug(exc)
except Exception:
continue
yield inv["payment_hash"]
except Exception as exc:
logger.debug(exc)
except Exception:
pass
# do not sleep if the connection was active for more than 10s
# since the backend is expected to drop the connection after 90s
+3 -6
View File
@@ -2,8 +2,6 @@ import base64
from getpass import getpass
from typing import Optional
from loguru import logger
from lnbits.utils.crypto import AESCipher
@@ -22,8 +20,7 @@ def load_macaroon(
aes = AESCipher(key.encode())
return aes.decrypt(encrypted_macaroon)
if not macaroon:
raise ValueError("macaroon must be set here")
assert macaroon, "macaroon must be set here"
# if the macaroon is a file path, load it and return hex version
if macaroon.split(".")[-1] == "macaroon":
@@ -42,7 +39,7 @@ def load_macaroon(
try:
macaroon = base64.b64decode(macaroon).hex()
return macaroon
except Exception as exc:
logger.debug(exc)
except Exception:
pass
return macaroon
+4 -5
View File
@@ -62,7 +62,7 @@ class NWCWallet(Wallet):
# interval in seconds between checks for pending payments
self.pending_payments_lookup_interval = 10
# track paid invoices for paid_invoices_stream
self.paid_invoices_queue = asyncio.Queue(0)
self.paid_invoices_queue: asyncio.Queue = asyncio.Queue(0)
# This task periodically checks if pending payments have been settled
self.pending_payments_lookup_task = asyncio.create_task(
self._handle_pending_payments()
@@ -309,8 +309,7 @@ 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
if not self.account_public_key:
raise ValueError("Missing account public key")
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)
@@ -391,7 +390,7 @@ class NWCConnection:
n = max_length - len(subid)
if n > 0:
for _ in range(n):
subid += chars[random.randint(0, len(chars) - 1)] # noqa: S311
subid += chars[random.randint(0, len(chars) - 1)]
return subid
async def _close_subscription_by_subid(
@@ -514,7 +513,7 @@ class NWCConnection:
if subscription: # Check if the subscription exists first
subscription["future"].set_exception(Exception(info))
async def _on_event_message(self, msg: list[Union[str, dict]]): # noqa: C901
async def _on_event_message(self, msg: list[Union[str, dict]]):
"""
Handles EVENT messages from the relay.
"""
+29 -82
View File
@@ -7,7 +7,6 @@ from collections.abc import AsyncGenerator
from typing import Any, Optional
import httpx
from httpx import RequestError, TimeoutException
from loguru import logger
from websockets.legacy.client import connect
@@ -16,7 +15,6 @@ from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -175,29 +173,13 @@ class PhoenixdWallet(Wallet):
)
r.raise_for_status()
except TimeoutException:
# be safe and return pending on timeouts
msg = f"Timeout connecting to {self.endpoint}. keep pending..."
logger.warning(msg)
return PaymentResponse(ok=None, error_message=msg)
except RequestError as exc:
# RequestError is raised when the request never hit the destination server
msg = f"Unable to connect to {self.endpoint}."
logger.warning(msg)
logger.warning(exc)
return PaymentResponse(ok=False, error_message=msg)
except Exception as exc:
logger.warning(exc)
return PaymentResponse(
ok=None, error_message=f"Unable to connect to {self.endpoint}."
)
try:
data = r.json()
if "routingFeeSat" not in data and ("reason" in data or "message" in data):
error_message = data.get("reason", data.get("message", "Unknown error"))
if "routingFeeSat" not in data and "reason" in data:
return PaymentResponse(error_message=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)
checking_id = data["paymentHash"]
@@ -226,74 +208,39 @@ class PhoenixdWallet(Wallet):
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(
f"/payments/incoming/{checking_id}?all=true&limit=1000"
)
if r.is_error:
if r.status_code == 404:
# invoice does not exist in phoenixd, so it was never paid
return PaymentFailedStatus()
else:
# otherwise something unexpected happened, and we keep it pending
logger.warning(
f"Error getting invoice status: {r.text}, keeping pending"
)
return PaymentPendingStatus()
try:
r = await self.client.get(f"/payments/incoming/{checking_id}")
if r.is_error:
return PaymentPendingStatus()
data = r.json()
except json.JSONDecodeError:
# should never return invalid json, but just in case we keep it pending
logger.warning(
f"Phoenixd: invalid json response: {r.text}, keeping pending"
)
if data["isPaid"]:
fee_msat = data["fees"]
preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
return PaymentPendingStatus()
if "isPaid" not in data or "fees" not in data or "preimage" not in data:
# should never return missing fields, but just in case we keep it pending
except Exception as e:
logger.error(f"Error getting invoice status: {e}")
return PaymentPendingStatus()
if data["isPaid"] is True:
fee_msat = data["fees"]
preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(
f"/payments/outgoing/{checking_id}?all=true&limit=1000"
)
if r.is_error:
if r.status_code == 404:
# payment does not exist in phoenixd, so it was never paid
return PaymentFailedStatus()
else:
# otherwise something unexpected happened, and we keep it pending
logger.warning(
f"Error getting payment status: {r.text}, keeping pending"
)
return PaymentPendingStatus()
# TODO: how can we detect a failed payment?
try:
r = await self.client.get(f"/payments/outgoing/{checking_id}")
if r.is_error:
return PaymentPendingStatus()
data = r.json()
except json.JSONDecodeError:
# should never return invalid json, but just in case we keep it pending
logger.warning(
f"Phoenixd: invalid json response: {r.text}, keeping pending"
)
return PaymentPendingStatus()
if "isPaid" not in data or "fees" not in data or "preimage" not in data:
# should never happen, but just in case we keep it pending
logger.warning(
f"Phoenixd: missing required fields: {data}, keeping pending"
)
return PaymentPendingStatus()
if data["isPaid"]:
fee_msat = data["fees"]
preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
if data["isPaid"] is True:
fee_msat = data["fees"]
preimage = data["preimage"]
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
return PaymentPendingStatus()
return PaymentPendingStatus()
except Exception as e:
logger.error(f"Error getting invoice status: {e}")
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
@@ -316,7 +263,7 @@ class PhoenixdWallet(Wallet):
yield message_json["paymentHash"]
except Exception as exc:
logger.warning(
logger.error(
f"lost connection to phoenixd invoices stream: '{exc}'"
"retrying in 5 seconds"
)
+2 -2
View File
@@ -1,8 +1,8 @@
import asyncio
import hashlib
import json
import random
from collections.abc import AsyncGenerator
from secrets import token_urlsafe
from typing import Optional
import httpx
@@ -116,7 +116,7 @@ class SparkWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
label = f"lbs{token_urlsafe(16)}"
label = f"lbs{random.random()}"
try:
if description_hash:
r = await self.invoicewithdescriptionhash(
+4 -4
View File
@@ -27,7 +27,7 @@ class TokenBucket:
Token bucket rate limiter for Strike API endpoints.
"""
def __init__(self, rate: int, period_seconds: int) -> None:
def __init__(self, rate: int, period_seconds: int):
"""
Initialize a token bucket.
@@ -77,7 +77,7 @@ class StrikeWallet(Wallet):
# construction / teardown #
# --------------------------------------------------------------------- #
def __init__(self) -> None:
def __init__(self):
if not settings.strike_api_endpoint:
raise ValueError("Missing strike_api_endpoint")
if not settings.strike_api_key:
@@ -120,7 +120,7 @@ class StrikeWallet(Wallet):
self._cached_balance_ts: float = 0.0
self._cache_ttl = 30 # seconds
async def cleanup(self) -> None:
async def cleanup(self):
try:
await self.client.aclose()
except Exception:
@@ -487,7 +487,7 @@ class StrikeWallet(Wallet):
return None
async def _get_payment_status_by_checking_id( # noqa: C901
async def _get_payment_status_by_checking_id(
self, checking_id: str
) -> PaymentStatus:
r_payment = await self._get(f"/payments/{checking_id}")
Generated
+14 -135
View File
@@ -656,47 +656,6 @@ files = [
{file = "breez_sdk-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e51689ed2f2c2d49bdb53f0ee26d0dc2109a50f15b8f2129a250db275eb994bf"},
]
[[package]]
name = "breez-sdk-liquid"
version = "0.9.1"
description = "Python language bindings for the Breez Liquid SDK"
optional = true
python-versions = "*"
groups = ["main"]
markers = "extra == \"breez\""
files = [
{file = "breez_sdk_liquid-0.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:414a7c04dbf869435fe234b3b9987699c15dc2f8d4660d9ca1a51af608331b45"},
{file = "breez_sdk_liquid-0.9.1-cp310-cp310-manylinux_2_31_aarch64.whl", hash = "sha256:7f461b6b10ddfe9cfb58119e590e5f67da4d7c93fc580a84a0116ce9fd21dffa"},
{file = "breez_sdk_liquid-0.9.1-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:c27efe35539e4092dddbe5315f5cefffccbdd699017ed8733611d11eb52f4ce0"},
{file = "breez_sdk_liquid-0.9.1-cp310-cp310-win32.whl", hash = "sha256:375f6322c2552d15d6ba68302a85fdc345a7671eeda8fea92c013a16c5e96505"},
{file = "breez_sdk_liquid-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:f28d107099fb7caaac4051d5950a4f11bf823e11b56e797976c5fa2a7280d597"},
{file = "breez_sdk_liquid-0.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acefa49896bdb4ca06d351faacbf075646d574dda5539033df11310ec5a94446"},
{file = "breez_sdk_liquid-0.9.1-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:d0e54fd5c8ce542dc2061d8a1ef46e6a1d430dbaea5eab369bb91ae2c4a1be7b"},
{file = "breez_sdk_liquid-0.9.1-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:a5d1bfd4d3286d57c110d925332416b0903dda076dadede02c4b7db67576f0bf"},
{file = "breez_sdk_liquid-0.9.1-cp311-cp311-win32.whl", hash = "sha256:3ce2152f0a2ba65f4425fd22efb8e1dafb419e5fcf3b93dfe06fc061b1d1c0c6"},
{file = "breez_sdk_liquid-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:8cdb4f247bccc0f4b9bbfff4451faa78d7b17e76a9dc918804880b600f1d4249"},
{file = "breez_sdk_liquid-0.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c463191a89a829afcae19c94f7683dac415667755d68c212afc60d8f2b21ba63"},
{file = "breez_sdk_liquid-0.9.1-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:52efaeae2c278b498b37de1090e80f73987ee943743e69a363877ed93cd7a31e"},
{file = "breez_sdk_liquid-0.9.1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:68aebea98990319c677bcbd7d1de258627b85063a29720879986694b9d393adb"},
{file = "breez_sdk_liquid-0.9.1-cp312-cp312-win32.whl", hash = "sha256:f53e4a1973dfd41df3ac3ecb42cf72acb71f49d5727a8acab25668b51a953bb2"},
{file = "breez_sdk_liquid-0.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:6adb1f3f3ccd4b743a11dcc0b307db604c3424f42e2547f94fa939f0afb9bb85"},
{file = "breez_sdk_liquid-0.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f94432a75bc848e597fb9859a77bab6c703d5e423d470519f37c402fc3c5d79e"},
{file = "breez_sdk_liquid-0.9.1-cp313-cp313-manylinux_2_31_aarch64.whl", hash = "sha256:8d6838e69620162c038d513f95ffade870e112ae5c6a0d713eaa2a3a31d6f212"},
{file = "breez_sdk_liquid-0.9.1-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:d6b9a8601cef5875447e10c75ca3e1579dd1e881ec57cabe9787616b8a6de263"},
{file = "breez_sdk_liquid-0.9.1-cp313-cp313-win32.whl", hash = "sha256:d76fc905003ac5a0cfd2132df90ba6343b40b9948164a0be06a68f15b28789a2"},
{file = "breez_sdk_liquid-0.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6b0fc1d77b7855d9a72799d65cdaaa0bd07a3999d6d4d27050d8ef174ce0fd3"},
{file = "breez_sdk_liquid-0.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d0d560657c29d7e3b054ad4be07955fc41b63d4b93a2b51b29bb85d1d52341af"},
{file = "breez_sdk_liquid-0.9.1-cp38-cp38-manylinux_2_31_aarch64.whl", hash = "sha256:2531f788eb15bb00f4b3b505ac5b8ee3de23161dd4f65768a4aa3255ea04d68e"},
{file = "breez_sdk_liquid-0.9.1-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:f9c9f6255999cf403bc07a9deed99f8c1625a0405ae9231e3dbecb3b6af0fdc7"},
{file = "breez_sdk_liquid-0.9.1-cp38-cp38-win32.whl", hash = "sha256:5232b25b7e8583100ec54423e7568a305cac7778736adc9f8c735798110350c7"},
{file = "breez_sdk_liquid-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:67d2b894b70ba9011fc98e04ed1ade2ff18f0413b6f2a0994747e71418d004e4"},
{file = "breez_sdk_liquid-0.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bd77d2ce43a2c0ad77a7f22226e373034ff57677338b38e4ebe3b77eb34fa224"},
{file = "breez_sdk_liquid-0.9.1-cp39-cp39-manylinux_2_31_aarch64.whl", hash = "sha256:14092d9d65f1b0a9009e64630614e7b8b936e9d382d0f6415f023f0b192f6b45"},
{file = "breez_sdk_liquid-0.9.1-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:5faa4d5e9a2b47899783aa0c44ac2274d32f74a7f469506af14520deabaabf9f"},
{file = "breez_sdk_liquid-0.9.1-cp39-cp39-win32.whl", hash = "sha256:b5b2c4bae13655d108475f4f22efdc2b07283f72d99b9c6f99166d9e41e62942"},
{file = "breez_sdk_liquid-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:f3ad7c65582558031451538c8a03caa3486a2d2c8906f2fbbccce84e9357d1ea"},
]
[[package]]
name = "certifi"
version = "2025.6.15"
@@ -2755,102 +2714,23 @@ files = [
[[package]]
name = "protobuf"
version = "5.29.5"
version = "5.29.1"
description = ""
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"},
{file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"},
{file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"},
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"},
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"},
{file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"},
{file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"},
{file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"},
{file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"},
{file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"},
{file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"},
]
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"migration\""
files = [
{file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"},
{file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"},
{file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"},
{file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
{file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"},
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"},
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
{file = "protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110"},
{file = "protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34"},
{file = "protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18"},
{file = "protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155"},
{file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"},
{file = "protobuf-5.29.1-cp38-cp38-win32.whl", hash = "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853"},
{file = "protobuf-5.29.1-cp38-cp38-win_amd64.whl", hash = "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331"},
{file = "protobuf-5.29.1-cp39-cp39-win32.whl", hash = "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57"},
{file = "protobuf-5.29.1-cp39-cp39-win_amd64.whl", hash = "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c"},
{file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"},
{file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"},
]
[[package]]
@@ -4493,11 +4373,10 @@ multidict = ">=4.0"
propcache = ">=0.2.1"
[extras]
breez = ["breez-sdk", "breez-sdk-liquid"]
breez = ["breez-sdk"]
liquid = ["wallycore"]
migration = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = "~3.12 | ~3.11 | ~3.10"
content-hash = "265fb5ec81a3c703ca7db07dfe206194fbb8616cb6e196d0917f33e6df58753c"
content-hash = "434e37e5aeb0eeb1a22e12fd1914b45f27195162ef4ebc1055071fd4718d85fc"
+6 -31
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lnbits"
version = "1.2.0"
version = "1.2.1"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
readme = "README.md"
@@ -33,7 +33,7 @@ uvloop = "0.21.0"
websockets = "15.0.1"
loguru = "0.7.3"
grpcio = "1.69.0"
protobuf = "5.29.5"
protobuf = "5.29.1"
pyln-client = "25.5"
pywebpush = "2.0.3"
slowapi = "0.1.9"
@@ -57,9 +57,6 @@ python-crontab = "3.2.0"
wallycore = {version = "1.4.0", optional = true}
# needed for breez funding source
breez-sdk = {version = "0.8.0", optional = true}
breez-sdk-liquid = {version = "0.9.1", optional = true}
# needed for migration tests
psycopg2-binary = {version = "2.9.10", optional = true}
jsonpath-ng = "^1.7.0"
pynostr = "^0.6.2"
@@ -68,9 +65,8 @@ filetype = "^1.2.0"
nostr-sdk = "^0.42.1"
[tool.poetry.extras]
breez = ["breez-sdk", "breez-sdk-liquid"]
breez = ["breez-sdk"]
liquid = ["wallycore"]
migration = ["psycopg2-binary"]
[tool.poetry.group.dev.dependencies]
black = "^25.1.0"
@@ -151,8 +147,6 @@ module = [
"json5.*",
"jsonpath_ng.*",
"filetype.*",
"breez_sdk.*",
"breez_sdk_liquid.*",
]
ignore_missing_imports = "True"
@@ -197,8 +191,7 @@ extend-exclude = [
# UP - pyupgrade
# RUF - ruff specific rules
# B - bugbear
# S - bandit
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B", "S"]
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
# RUF012: mutable-class-default
ignore = ["RUF012"]
@@ -217,27 +210,9 @@ classmethod-decorators = [
"validator",
]
[tool.ruff.lint.per-file-ignores]
# S101: Use of assert detected. mostly for tests...
# S105: Use of hard-coded password. mostly for tests...
# S106: Possible hardcoded password: 'password'.
# S307 Use of possibly insecure function; consider using `ast.literal_eval
# S602 `subprocess` call with `shell=True` identified, security issue
# S603 `subprocess` call: check for execution of untrusted input
# S607: Starting a process with a partial executable path
# S608: Possible SQL injection vector through string-based query construction
# S324 Probable use of insecure hash functions in `hashlib`: `md5`
# TODO: remove S101 ignore
"lnbits/*" = ["S101"]
"lnbits/core/views/admin_api.py" = ["S602", "S603", "S607"]
"crypto.py" = ["S324"]
"test*.py" = ["S101", "S105", "S106", "S307"]
"tools*.py" = ["S101", "S608"]
"tests/*" = ["S311"]
"tests/regtest/helpers.py" = ["S603"]
[tool.ruff.lint.mccabe]
max-complexity = 10
# TODO: Decrease this to 10.
max-complexity = 16
[tool.ruff.lint.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
-1
View File
@@ -307,7 +307,6 @@ def _settings_cleanup(settings: Settings):
settings.lnbits_reserve_fee_percent = 1
settings.lnbits_reserve_fee_min = 2000
settings.lnbits_service_fee = 0
settings.lnbits_reserve_fee_percent = 0
settings.lnbits_wallet_limit_daily_max_withdraw = 0
settings.lnbits_admin_extensions = []
settings.lnbits_admin_users = []
+1 -1
View File
@@ -81,7 +81,7 @@ async def test_crud_search_payments():
user = await create_user_account()
wallet = await create_wallet(user_id=user.id)
filters = Filters(
filters: Filters = Filters(
search="",
model=PaymentFilters,
)
+8 -16
View File
@@ -359,19 +359,16 @@ async def test_retry_failed_invoice(
@pytest.mark.anyio
async def test_pay_external_invoice_pending(
from_wallet: Wallet,
mocker: MockerFixture,
external_funding_source: FakeWallet,
settings: Settings,
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet
):
settings.lnbits_reserve_fee_min = 1000 # msats
invoice_amount = 2103
external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request
assert external_invoice.checking_id
preimage = "0000000000000000000000000000000000000000000000000000000000002103"
payment_reponse_pending = PaymentResponse(
ok=None, checking_id=external_invoice.checking_id
ok=None, checking_id=external_invoice.checking_id, preimage=preimage
)
mocker.patch(
"lnbits.wallets.FakeWallet.pay_invoice",
@@ -395,12 +392,12 @@ async def test_pay_external_invoice_pending(
assert _payment.checking_id == payment.payment_hash
assert _payment.amount == -2103_000
assert _payment.bolt11 == external_invoice.payment_request
assert _payment.preimage == preimage
wallet = await get_wallet(from_wallet.id)
assert wallet
reserve_fee_sat = int(abs(settings.lnbits_reserve_fee_min // 1000))
assert (
balance_before - invoice_amount - reserve_fee_sat == wallet.balance
balance_before - invoice_amount == wallet.balance
), "Pending payment is subtracted."
assert ws_notification.call_count == 0, "Websocket notification not sent."
@@ -408,12 +405,8 @@ async def test_pay_external_invoice_pending(
@pytest.mark.anyio
async def test_retry_pay_external_invoice_pending(
from_wallet: Wallet,
mocker: MockerFixture,
external_funding_source: FakeWallet,
settings: Settings,
from_wallet: Wallet, mocker: MockerFixture, external_funding_source: FakeWallet
):
settings.lnbits_reserve_fee_min = 2000 # msats
invoice_amount = 2106
external_invoice = await external_funding_source.create_invoice(invoice_amount)
assert external_invoice.payment_request
@@ -447,10 +440,9 @@ async def test_retry_pay_external_invoice_pending(
wallet = await get_wallet(from_wallet.id)
assert wallet
reserve_fee_sat = int(abs(settings.lnbits_reserve_fee_min // 1000))
# TODO: is this correct?
assert (
balance_before - invoice_amount - reserve_fee_sat == wallet.balance
balance_before - invoice_amount == wallet.balance
), "Failed payment is subtracted."
assert ws_notification.call_count == 0, "Websocket notification not sent."
+20 -20
View File
@@ -2182,16 +2182,8 @@
"get_invoice_status_endpoint": []
},
"phoenixd": {
"get_invoice_status_endpoint": [
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
"description": "phoenixd.py doesn't handle the 'failed' status for `get_invoice_status`",
"get_invoice_status_endpoint": []
}
}
},
@@ -2397,6 +2389,14 @@
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
@@ -2674,16 +2674,8 @@
]
},
"phoenixd": {
"get_payment_status_endpoint": [
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
"description": "phoenixd.py doesn't handle the 'failed' status for `get_invoice_status`",
"get_payment_status_endpoint": []
}
}
},
@@ -2940,6 +2932,14 @@
"description": "bad json",
"response_type": "data",
"response": "data-not-json"
},
{
"description": "http 404",
"response_type": "response",
"response": {
"response": "Not Found",
"status": 404
}
}
]
}
+1 -1
View File
@@ -74,7 +74,7 @@ def sign_event(pub_key, priv_key, event):
return event
async def handle(wallet, mock_settings, data, websocket, path): # noqa: C901
async def handle(wallet, mock_settings, data, websocket, path):
async for message in websocket:
if not wallet:
continue