Compare commits
50
Commits
v1.2.1
...
v1.3.0-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609c949394 | ||
|
|
c24d2f24cf | ||
|
|
0ba81ec719 | ||
|
|
5e03707be5 | ||
|
|
50f1111356 | ||
|
|
112a0f9698 | ||
|
|
75016d1a27 | ||
|
|
5b9f244a9f | ||
|
|
0d2ff2e663 | ||
|
|
bdfefc2506 | ||
|
|
2a9f471783 | ||
|
|
793c7c45ed | ||
|
|
ff535ae065 | ||
|
|
01fdc1defb | ||
|
|
dce2bfb440 | ||
|
|
a36ab2d408 | ||
|
|
98f732fb6f | ||
|
|
65864cb216 | ||
|
|
114e7b8c3c | ||
|
|
fbfab880f3 | ||
|
|
133afff8ee | ||
|
|
851c952c6d | ||
|
|
8b29148085 | ||
|
|
ad48092549 | ||
|
|
ee449ca612 | ||
|
|
73f45ae859 | ||
|
|
094a9fc1a1 | ||
|
|
5e9919c5c5 | ||
|
|
f91c933919 | ||
|
|
6e9f451419 | ||
|
|
256a8098c6 | ||
|
|
1a02095758 | ||
|
|
ec5b7493b3 | ||
|
|
cdc1a6dc8f | ||
|
|
7d573ef859 | ||
|
|
4bd2a4fba9 | ||
|
|
209c251fb6 | ||
|
|
69a7ef74fd | ||
|
|
35e2c4b0a7 | ||
|
|
2c6dce808e | ||
|
|
fa89313e6f | ||
|
|
76ecf113c3 | ||
|
|
e0749e186e | ||
|
|
0529ee2835 | ||
|
|
09b91f9f79 | ||
|
|
90ab642dcd | ||
|
|
88cf1ac853 | ||
|
|
9aa2194d94 | ||
|
|
ba0f7a01ef | ||
|
|
5765ea0276 |
+21
-1
@@ -40,7 +40,7 @@ PORT=5000
|
|||||||
######################################
|
######################################
|
||||||
|
|
||||||
# which fundingsources are allowed in the admin ui
|
# 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"
|
# 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_BACKEND_WALLET_CLASS=VoidWallet
|
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
|
||||||
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
|
||||||
@@ -61,6 +61,20 @@ CLICHE_ENDPOINT=ws://127.0.0.1:12000
|
|||||||
SPARK_URL=http://localhost:9737/rpc
|
SPARK_URL=http://localhost:9737/rpc
|
||||||
SPARK_TOKEN=myaccesstoken
|
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
|
# CoreLightningWallet
|
||||||
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
|
||||||
|
|
||||||
@@ -153,6 +167,12 @@ 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_GREENLIGHT_DEVICE_CERT="/path/to/breezsdk/device.crt" # or BASE64/HEXSTRING
|
||||||
# BREEZ_USE_TRAMPOLINE=true
|
# 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
|
# Google OAuth Config
|
||||||
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
poetry env use python${{ inputs.python-version }}
|
poetry env use python${{ inputs.python-version }}
|
||||||
poetry install
|
poetry install --all-extras
|
||||||
# needed for conv tests
|
|
||||||
poetry add psycopg2-binary
|
|
||||||
|
|
||||||
- name: Use Node.js ${{ inputs.node-version }}
|
- name: Use Node.js ${{ inputs.node-version }}
|
||||||
if: ${{ (inputs.npm == 'true') }}
|
if: ${{ (inputs.npm == 'true') }}
|
||||||
|
|||||||
@@ -12,6 +12,23 @@ 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).
|
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
|
### CoreLightning
|
||||||
|
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
||||||
@@ -19,6 +36,8 @@ You can [compare the LNbits compatible Lightning Network funding sources here](w
|
|||||||
|
|
||||||
### CoreLightning REST
|
### 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**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
|
||||||
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
|
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
|
||||||
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
|
||||||
@@ -135,6 +154,18 @@ 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_KEY`: /path/to/breezsdk/device.pem or Base64/Hex
|
||||||
- `BREEZ_GREENLIGHT_DEVICE_CERT`: /path/to/breezsdk/device.crt 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 Wallet
|
||||||
|
|
||||||
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||||
|
|||||||
+15
-13
@@ -4,6 +4,7 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
@@ -26,7 +27,8 @@ from lnbits.core.crud.extensions import create_installed_extension
|
|||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
from lnbits.core.services.notifications import enqueue_admin_notification
|
||||||
|
from lnbits.core.services.payments import check_pending_payments
|
||||||
from lnbits.core.tasks import (
|
from lnbits.core.tasks import (
|
||||||
audit_queue,
|
audit_queue,
|
||||||
collect_exchange_rates_data,
|
collect_exchange_rates_data,
|
||||||
@@ -65,16 +67,13 @@ from .middleware import (
|
|||||||
add_ip_block_middleware,
|
add_ip_block_middleware,
|
||||||
add_ratelimit_middleware,
|
add_ratelimit_middleware,
|
||||||
)
|
)
|
||||||
from .tasks import (
|
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
||||||
check_pending_payments,
|
|
||||||
internal_invoice_listener,
|
|
||||||
invoice_listener,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def startup(app: FastAPI):
|
async def startup(app: FastAPI):
|
||||||
|
logger.info(f"Starting LNbits Version: {settings.version}")
|
||||||
|
start = time.perf_counter()
|
||||||
settings.lnbits_running = True
|
settings.lnbits_running = True
|
||||||
|
|
||||||
# wait till migration is done
|
# wait till migration is done
|
||||||
await migrate_databases()
|
await migrate_databases()
|
||||||
|
|
||||||
@@ -105,7 +104,7 @@ async def startup(app: FastAPI):
|
|||||||
# initialize tasks
|
# initialize tasks
|
||||||
register_async_tasks()
|
register_async_tasks()
|
||||||
|
|
||||||
enqueue_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.server_start_stop,
|
NotificationType.server_start_stop,
|
||||||
{
|
{
|
||||||
"message": "LNbits server started.",
|
"message": "LNbits server started.",
|
||||||
@@ -113,10 +112,13 @@ async def startup(app: FastAPI):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
end = time.perf_counter()
|
||||||
|
logger.success(f"LNbits started in {end - start:.2f} seconds.")
|
||||||
|
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
logger.warning("LNbits shutting down...")
|
logger.warning("LNbits shutting down...")
|
||||||
enqueue_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.server_start_stop,
|
NotificationType.server_start_stop,
|
||||||
{
|
{
|
||||||
"message": "LNbits server shutting down...",
|
"message": "LNbits server shutting down...",
|
||||||
@@ -277,7 +279,7 @@ async def check_installed_extensions(app: FastAPI):
|
|||||||
logger.info(f"{ext.id} ({ext.installed_version})")
|
logger.info(f"{ext.id} ({ext.installed_version})")
|
||||||
|
|
||||||
|
|
||||||
async def build_all_installed_extensions_list(
|
async def build_all_installed_extensions_list( # noqa: C901
|
||||||
include_deactivated: Optional[bool] = True,
|
include_deactivated: Optional[bool] = True,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
"""
|
"""
|
||||||
@@ -451,7 +453,7 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
|||||||
app.include_router(router=ext_route, prefix=prefix)
|
app.include_router(router=ext_route, prefix=prefix)
|
||||||
|
|
||||||
|
|
||||||
async def check_and_register_extensions(app: FastAPI):
|
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||||
await check_installed_extensions(app)
|
await check_installed_extensions(app)
|
||||||
for ext in await get_valid_extensions(False):
|
for ext in await get_valid_extensions(False):
|
||||||
try:
|
try:
|
||||||
@@ -461,12 +463,12 @@ async def check_and_register_extensions(app: FastAPI):
|
|||||||
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
|
||||||
|
|
||||||
|
|
||||||
def register_async_tasks():
|
def register_async_tasks() -> None:
|
||||||
|
|
||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(wait_notification_messages)
|
create_permanent_task(wait_notification_messages)
|
||||||
|
|
||||||
create_permanent_task(check_pending_payments)
|
create_permanent_task(run_interval(30 * 60, check_pending_payments))
|
||||||
create_permanent_task(invoice_listener)
|
create_permanent_task(invoice_listener)
|
||||||
create_permanent_task(internal_invoice_listener)
|
create_permanent_task(internal_invoice_listener)
|
||||||
create_permanent_task(cache.invalidate_forever)
|
create_permanent_task(cache.invalidate_forever)
|
||||||
|
|||||||
+7
-7
@@ -320,9 +320,8 @@ async def extensions_list():
|
|||||||
from lnbits.app import build_all_installed_extensions_list
|
from lnbits.app import build_all_installed_extensions_list
|
||||||
|
|
||||||
for ext in await build_all_installed_extensions_list():
|
for ext in await build_all_installed_extensions_list():
|
||||||
assert (
|
if not ext.meta or not ext.meta.installed_release:
|
||||||
ext.meta and ext.meta.installed_release
|
raise ValueError(f"Extension {ext.id} has no installed_release")
|
||||||
), f"Extension {ext.id} has no installed_release"
|
|
||||||
click.echo(f" - {ext.id} ({ext.meta.installed_release.version})")
|
click.echo(f" - {ext.id} ({ext.meta.installed_release.version})")
|
||||||
|
|
||||||
|
|
||||||
@@ -353,7 +352,7 @@ async def extensions_list():
|
|||||||
help="Admin user ID (must have permissions to install extensions).",
|
help="Admin user ID (must have permissions to install extensions).",
|
||||||
)
|
)
|
||||||
@coro
|
@coro
|
||||||
async def extensions_update(
|
async def extensions_update( # noqa: C901
|
||||||
extension: Optional[str] = None,
|
extension: Optional[str] = None,
|
||||||
all_extensions: Optional[bool] = False,
|
all_extensions: Optional[bool] = False,
|
||||||
repo_index: Optional[str] = None,
|
repo_index: Optional[str] = None,
|
||||||
@@ -608,9 +607,10 @@ async def update_extension(
|
|||||||
|
|
||||||
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
|
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
|
||||||
|
|
||||||
assert (
|
if not installed_ext.meta or not installed_ext.meta.installed_release:
|
||||||
installed_ext.meta and installed_ext.meta.installed_release
|
raise ValueError(
|
||||||
), "Cannot find previously installed release. Please uninstall first."
|
"Cannot find previously installed release. Please uninstall first."
|
||||||
|
)
|
||||||
|
|
||||||
release = await _select_release(extension, repo_index, source_repo)
|
release = await _select_release(extension, repo_index, source_repo)
|
||||||
if not release:
|
if not release:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .views.fiat_api import fiat_router
|
|||||||
|
|
||||||
# this compat is needed for usermanager extension
|
# this compat is needed for usermanager extension
|
||||||
from .views.generic import generic_router
|
from .views.generic import generic_router
|
||||||
|
from .views.lnurl_api import lnurl_router
|
||||||
from .views.node_api import node_router, public_node_router, super_node_router
|
from .views.node_api import node_router, public_node_router, super_node_router
|
||||||
from .views.payment_api import payment_router
|
from .views.payment_api import payment_router
|
||||||
from .views.tinyurl_api import tinyurl_router
|
from .views.tinyurl_api import tinyurl_router
|
||||||
@@ -42,6 +43,7 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
|
app.include_router(lnurl_router)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["core_app", "core_app_extra", "db"]
|
__all__ = ["core_app", "core_app_extra", "db"]
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ async def delete_expired_audit_entries(
|
|||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
):
|
):
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
DELETE from audit
|
DELETE from audit
|
||||||
WHERE delete_at < {db.timestamp_now}
|
WHERE delete_at < {db.timestamp_now}
|
||||||
""",
|
""", # noqa: S608
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,13 +49,16 @@ async def get_count_stats(
|
|||||||
filters = Filters()
|
filters = Filters()
|
||||||
clause = filters.where()
|
clause = filters.where()
|
||||||
data = await (conn or db).fetchall(
|
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"""
|
query=f"""
|
||||||
SELECT {field} as field, count({field}) as total
|
SELECT {field} as field, count({field}) as total
|
||||||
FROM audit
|
FROM audit
|
||||||
{clause}
|
{clause}
|
||||||
GROUP BY {field}
|
GROUP BY {field}
|
||||||
ORDER BY {field}
|
ORDER BY {field}
|
||||||
""",
|
""", # noqa: S608
|
||||||
values=filters.values(),
|
values=filters.values(),
|
||||||
model=AuditCountStat,
|
model=AuditCountStat,
|
||||||
)
|
)
|
||||||
@@ -70,13 +74,15 @@ async def get_long_duration_stats(
|
|||||||
filters = Filters()
|
filters = Filters()
|
||||||
clause = filters.where()
|
clause = filters.where()
|
||||||
long_duration_paths = await (conn or db).fetchall(
|
long_duration_paths = await (conn or db).fetchall(
|
||||||
|
# This query is safe from SQL injection
|
||||||
|
# The `clause` is constructed from sanitized inputs
|
||||||
query=f"""
|
query=f"""
|
||||||
SELECT path as field, max(duration) as total FROM audit
|
SELECT path as field, max(duration) as total FROM audit
|
||||||
{clause}
|
{clause}
|
||||||
GROUP BY path
|
GROUP BY path
|
||||||
ORDER BY total DESC
|
ORDER BY total DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
""",
|
""", # noqa: S608
|
||||||
values=filters.values(),
|
values=filters.values(),
|
||||||
model=AuditCountStat,
|
model=AuditCountStat,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ async def drop_extension_db(ext_id: str, conn: Optional[Connection] = None) -> N
|
|||||||
{"id": ext_id},
|
{"id": ext_id},
|
||||||
)
|
)
|
||||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||||
assert row, f"Extension '{ext_id}' db version cannot be found"
|
if not row:
|
||||||
|
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||||
|
|
||||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||||
if is_file_based_db:
|
if is_file_based_db:
|
||||||
@@ -78,10 +79,13 @@ async def get_installed_extensions(
|
|||||||
active: Optional[bool] = None,
|
active: Optional[bool] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[InstallableExtension]:
|
) -> list[InstallableExtension]:
|
||||||
where = "WHERE active = :active" if active is not None else ""
|
query = "SELECT * FROM installed_extensions"
|
||||||
|
if active is not None:
|
||||||
|
query += " WHERE active = :active"
|
||||||
|
|
||||||
values = {"active": active} if active is not None else {}
|
values = {"active": active} if active is not None else {}
|
||||||
all_extensions = await (conn or db).fetchall(
|
all_extensions = await (conn or db).fetchall(
|
||||||
f"SELECT * FROM installed_extensions {where}",
|
query,
|
||||||
values,
|
values,
|
||||||
model=InstallableExtension,
|
model=InstallableExtension,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ async def get_standalone_payment(
|
|||||||
clause = f"({clause}) AND wallet_id = :wallet_id"
|
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||||
|
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
|
# This query is safe from SQL injection
|
||||||
|
# The `clause` is constructed from sanitized inputs
|
||||||
f"""
|
f"""
|
||||||
SELECT * FROM apipayments
|
SELECT * FROM apipayments
|
||||||
WHERE {clause}
|
WHERE {clause}
|
||||||
ORDER BY amount LIMIT 1
|
ORDER BY amount LIMIT 1
|
||||||
""",
|
""", # noqa: S608
|
||||||
values,
|
values,
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
@@ -80,19 +82,25 @@ async def get_latest_payments_by_extension(
|
|||||||
ext_name: str, ext_id: str, limit: int = 5
|
ext_name: str, ext_id: str, limit: int = 5
|
||||||
) -> list[Payment]:
|
) -> list[Payment]:
|
||||||
return await db.fetchall(
|
return await db.fetchall(
|
||||||
|
# This query is safe from SQL injection
|
||||||
|
# The limtit is an integer and not user input
|
||||||
f"""
|
f"""
|
||||||
SELECT * FROM apipayments
|
SELECT * FROM apipayments
|
||||||
WHERE status = '{PaymentState.SUCCESS}'
|
WHERE status = :status
|
||||||
AND extra LIKE :ext_name
|
AND extra LIKE :ext_name
|
||||||
AND extra LIKE :ext_id
|
AND extra LIKE :ext_id
|
||||||
ORDER BY time DESC LIMIT {int(limit)}
|
ORDER BY time DESC LIMIT {int(limit)}
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"ext_name": f"%{ext_name}%", "ext_id": f"%{ext_id}%"},
|
{
|
||||||
|
"status": f"{PaymentState.SUCCESS}",
|
||||||
|
"ext_name": f"%{ext_name}%",
|
||||||
|
"ext_id": f"%{ext_id}%",
|
||||||
|
},
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_paginated(
|
async def get_payments_paginated( # noqa: C901
|
||||||
*,
|
*,
|
||||||
wallet_id: Optional[str] = None,
|
wallet_id: Optional[str] = None,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
@@ -227,21 +235,23 @@ async def delete_expired_invoices(
|
|||||||
# first we delete all invoices older than one month
|
# first we delete all invoices older than one month
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
DELETE FROM apipayments
|
DELETE FROM apipayments
|
||||||
WHERE status = '{PaymentState.PENDING}' AND amount > 0
|
WHERE status = :status AND amount > 0
|
||||||
AND time < {db.timestamp_placeholder("delta")}
|
AND time < {db.timestamp_placeholder("delta")}
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"delta": int(time() - 2592000)},
|
{"status": f"{PaymentState.PENDING}", "delta": int(time() - 2592000)},
|
||||||
)
|
)
|
||||||
# then we delete all invoices whose expiry date is in the past
|
# then we delete all invoices whose expiry date is in the past
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
DELETE FROM apipayments
|
DELETE FROM apipayments
|
||||||
WHERE status = '{PaymentState.PENDING}' AND amount > 0
|
WHERE status = :status AND amount > 0
|
||||||
AND expiry < {db.timestamp_placeholder("now")}
|
AND expiry < {db.timestamp_placeholder("now")}
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"now": int(time())},
|
{"status": f"{PaymentState.PENDING}", "now": int(time())},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -254,7 +264,8 @@ async def create_payment(
|
|||||||
# we don't allow the creation of the same invoice twice
|
# we don't allow the creation of the same invoice twice
|
||||||
# note: this can be removed if the db uniqueness constraints are set appropriately
|
# note: this can be removed if the db uniqueness constraints are set appropriately
|
||||||
previous_payment = await get_standalone_payment(checking_id, conn=conn)
|
previous_payment = await get_standalone_payment(checking_id, conn=conn)
|
||||||
assert previous_payment is None, "Payment already exists"
|
if previous_payment is not None:
|
||||||
|
raise ValueError("Payment already exists")
|
||||||
extra = data.extra or {}
|
extra = data.extra or {}
|
||||||
|
|
||||||
payment = Payment(
|
payment = Payment(
|
||||||
@@ -268,7 +279,7 @@ async def create_payment(
|
|||||||
preimage=data.preimage,
|
preimage=data.preimage,
|
||||||
expiry=data.expiry,
|
expiry=data.expiry,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
fee=data.fee,
|
fee=-abs(data.fee),
|
||||||
tag=extra.get("tag", None),
|
tag=extra.get("tag", None),
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
@@ -321,16 +332,20 @@ async def get_payments_history(
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
|
clause = filters.where(where)
|
||||||
transactions: list[dict] = await db.fetchall(
|
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"""
|
f"""
|
||||||
SELECT {date_trunc} date,
|
SELECT {date_trunc} date,
|
||||||
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) income,
|
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
|
SUM(CASE WHEN amount < 0 THEN abs(amount) + abs(fee) ELSE 0 END) spending
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
{filters.where(where)}
|
{clause}
|
||||||
GROUP BY date
|
GROUP BY date
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
""",
|
""", # noqa: S608
|
||||||
filters.values(values),
|
filters.values(values),
|
||||||
)
|
)
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
@@ -376,13 +391,16 @@ async def get_payment_count_stats(
|
|||||||
|
|
||||||
clause = filters.where(extra_stmts)
|
clause = filters.where(extra_stmts)
|
||||||
data = await (conn or db).fetchall(
|
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"""
|
query=f"""
|
||||||
SELECT {field} as field, count(*) as total
|
SELECT {field} as field, count(*) as total
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
{clause}
|
{clause}
|
||||||
GROUP BY {field}
|
GROUP BY {field}
|
||||||
ORDER BY {field}
|
ORDER BY {field}
|
||||||
""",
|
""", # noqa: S608
|
||||||
values=filters.values(),
|
values=filters.values(),
|
||||||
model=PaymentCountStat,
|
model=PaymentCountStat,
|
||||||
)
|
)
|
||||||
@@ -468,6 +486,8 @@ async def get_wallets_stats(
|
|||||||
clauses = filters.where(where_stmts)
|
clauses = filters.where(where_stmts)
|
||||||
|
|
||||||
data = await (conn or db).fetchall(
|
data = await (conn or db).fetchall(
|
||||||
|
# This query is safe from SQL injection
|
||||||
|
# The `clauses` is constructed from sanitized inputs
|
||||||
query=f"""
|
query=f"""
|
||||||
SELECT apipayments.wallet_id,
|
SELECT apipayments.wallet_id,
|
||||||
MAX(wallets.name) AS wallet_name,
|
MAX(wallets.name) AS wallet_name,
|
||||||
@@ -479,7 +499,7 @@ async def get_wallets_stats(
|
|||||||
{clauses}
|
{clauses}
|
||||||
GROUP BY apipayments.wallet_id
|
GROUP BY apipayments.wallet_id
|
||||||
ORDER BY payments_count
|
ORDER BY payments_count
|
||||||
""",
|
""", # noqa: S608
|
||||||
values=filters.values(),
|
values=filters.values(),
|
||||||
model=PaymentWalletStats,
|
model=PaymentWalletStats,
|
||||||
)
|
)
|
||||||
@@ -504,11 +524,11 @@ async def check_internal(
|
|||||||
otherwise None
|
otherwise None
|
||||||
"""
|
"""
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
f"""
|
"""
|
||||||
SELECT * FROM apipayments
|
SELECT * FROM apipayments
|
||||||
WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0
|
WHERE payment_hash = :hash AND status = :status AND amount > 0
|
||||||
""",
|
""",
|
||||||
{"hash": payment_hash},
|
{"status": f"{PaymentState.PENDING}", "hash": payment_hash},
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ async def update_admin_settings(
|
|||||||
async def update_super_user(super_user: str) -> SuperSettings:
|
async def update_super_user(super_user: str) -> SuperSettings:
|
||||||
await set_settings_field("super_user", super_user)
|
await set_settings_field("super_user", super_user)
|
||||||
settings = await get_super_settings()
|
settings = await get_super_settings()
|
||||||
assert settings, "updated super_user settings could not be retrieved"
|
if not settings:
|
||||||
|
raise ValueError("updated super_user settings could not be retrieved")
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +87,8 @@ async def create_admin_settings(super_user: str, new_settings: dict) -> SuperSet
|
|||||||
await set_settings_field(key, value)
|
await set_settings_field(key, value)
|
||||||
|
|
||||||
settings = await get_super_settings()
|
settings = await get_super_settings()
|
||||||
assert settings, "created admin settings could not be retrieved"
|
if not settings:
|
||||||
|
raise ValueError("created admin settings could not be retrieved")
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ async def delete_accounts_no_wallets(
|
|||||||
) -> None:
|
) -> None:
|
||||||
delta = int(time()) - time_delta
|
delta = int(time()) - time_delta
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
DELETE FROM accounts
|
DELETE FROM accounts
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
@@ -118,7 +119,7 @@ async def delete_accounts_no_wallets(
|
|||||||
(updated_at is null AND created_at < :delta)
|
(updated_at is null AND created_at < :delta)
|
||||||
OR updated_at < {db.timestamp_placeholder("delta")}
|
OR updated_at < {db.timestamp_placeholder("delta")}
|
||||||
)
|
)
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"delta": delta},
|
{"delta": delta},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+26
-21
@@ -48,11 +48,12 @@ async def delete_wallet(
|
|||||||
) -> None:
|
) -> None:
|
||||||
now = int(time())
|
now = int(time())
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE wallets
|
UPDATE wallets
|
||||||
SET deleted = :deleted, updated_at = {db.timestamp_placeholder('now')}
|
SET deleted = :deleted, updated_at = {db.timestamp_placeholder('now')}
|
||||||
WHERE id = :wallet AND "user" = :user
|
WHERE id = :wallet AND "user" = :user
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"wallet": wallet_id, "user": user_id, "deleted": deleted, "now": now},
|
{"wallet": wallet_id, "user": user_id, "deleted": deleted, "now": now},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,11 +72,12 @@ async def delete_wallet_by_id(
|
|||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE wallets
|
UPDATE wallets
|
||||||
SET deleted = true, updated_at = {db.timestamp_placeholder('now')}
|
SET deleted = true, updated_at = {db.timestamp_placeholder('now')}
|
||||||
WHERE id = :wallet
|
WHERE id = :wallet
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"wallet": wallet_id, "now": now},
|
{"wallet": wallet_id, "now": now},
|
||||||
)
|
)
|
||||||
return result.rowcount
|
return result.rowcount
|
||||||
@@ -107,14 +109,16 @@ async def delete_unused_wallets(
|
|||||||
async def get_wallet(
|
async def get_wallet(
|
||||||
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
wallet_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> Optional[Wallet]:
|
) -> Optional[Wallet]:
|
||||||
where = "AND deleted = :deleted" if deleted is not None else ""
|
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 "
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
f"""
|
query,
|
||||||
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": wallet_id, "deleted": deleted},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
@@ -123,14 +127,16 @@ async def get_wallet(
|
|||||||
async def get_wallets(
|
async def get_wallets(
|
||||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> list[Wallet]:
|
) -> list[Wallet]:
|
||||||
where = "AND deleted = :deleted" if deleted is not None else ""
|
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 "
|
||||||
return await (conn or db).fetchall(
|
return await (conn or db).fetchall(
|
||||||
f"""
|
query,
|
||||||
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},
|
{"user": user_id, "deleted": deleted},
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
@@ -162,12 +168,11 @@ async def get_wallets_paginated(
|
|||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
where = "AND deleted = :deleted" if deleted is not None else ""
|
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
||||||
|
if deleted is not None:
|
||||||
|
query += "AND deleted = :deleted"
|
||||||
result: list[dict] = await (conn or db).fetchall(
|
result: list[dict] = await (conn or db).fetchall(
|
||||||
f"""
|
query,
|
||||||
SELECT id FROM wallets
|
|
||||||
WHERE "user" = :user {where}
|
|
||||||
""",
|
|
||||||
{"user": user_id, "deleted": deleted},
|
{"user": user_id, "deleted": deleted},
|
||||||
)
|
)
|
||||||
return [row["id"] for row in result]
|
return [row["id"] for row in result]
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ async def create_webpush_subscription(
|
|||||||
{"endpoint": endpoint, "user": user, "data": data, "host": host},
|
{"endpoint": endpoint, "user": user, "data": data, "host": host},
|
||||||
)
|
)
|
||||||
subscription = await get_webpush_subscription(endpoint, user)
|
subscription = await get_webpush_subscription(endpoint, user)
|
||||||
assert subscription, "Newly created webpush subscription couldn't be retrieved"
|
if not subscription:
|
||||||
|
raise ValueError("Newly created webpush subscription couldn't be retrieved")
|
||||||
return subscription
|
return subscription
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ async def m007_set_invoice_expiries(db: Connection):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
SELECT bolt11, checking_id
|
SELECT bolt11, checking_id
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
@@ -222,7 +223,7 @@ async def m007_set_invoice_expiries(db: Connection):
|
|||||||
AND bolt11 IS NOT NULL
|
AND bolt11 IS NOT NULL
|
||||||
AND expiry IS NULL
|
AND expiry IS NULL
|
||||||
AND time < {db.timestamp_now}
|
AND time < {db.timestamp_now}
|
||||||
"""
|
""" # noqa: S608
|
||||||
)
|
)
|
||||||
rows = result.mappings().all()
|
rows = result.mappings().all()
|
||||||
if len(rows):
|
if len(rows):
|
||||||
@@ -242,13 +243,15 @@ async def m007_set_invoice_expiries(db: Connection):
|
|||||||
f" {invoice.payment_hash} to {expiration_date}"
|
f" {invoice.payment_hash} to {expiration_date}"
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE apipayments SET expiry = {db.timestamp_placeholder('expiry')}
|
UPDATE apipayments SET expiry = {db.timestamp_placeholder('expiry')}
|
||||||
WHERE checking_id = :checking_id AND amount > 0
|
WHERE checking_id = :checking_id AND amount > 0
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"expiry": expiration_date, "checking_id": checking_id},
|
{"expiry": expiration_date, "checking_id": checking_id},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
# this is necessary now because it may be the case that this migration will
|
# this is necessary now because it may be the case that this migration will
|
||||||
@@ -371,7 +374,8 @@ async def m014_set_deleted_wallets(db: Connection):
|
|||||||
"wallet": row.get("id"),
|
"wallet": row.get("id"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
# this is necessary now because it may be the case that this migration will
|
# this is necessary now because it may be the case that this migration will
|
||||||
@@ -454,17 +458,19 @@ async def m017_add_timestamp_columns_to_accounts_and_wallets(db: Connection):
|
|||||||
# set all to now where they are null
|
# set all to now where they are null
|
||||||
now = int(time())
|
now = int(time())
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE wallets SET created_at = {db.timestamp_placeholder('now')}
|
UPDATE wallets SET created_at = {db.timestamp_placeholder('now')}
|
||||||
WHERE created_at IS NULL
|
WHERE created_at IS NULL
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"now": now},
|
{"now": now},
|
||||||
)
|
)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE accounts SET created_at = {db.timestamp_placeholder('now')}
|
UPDATE accounts SET created_at = {db.timestamp_placeholder('now')}
|
||||||
WHERE created_at IS NULL
|
WHERE created_at IS NULL
|
||||||
""",
|
""", # noqa: S608
|
||||||
{"now": now},
|
{"now": now},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -616,7 +622,12 @@ async def m027_update_apipayments_data(db: Connection):
|
|||||||
logger.info(f"Updating {offset} to {offset+limit}")
|
logger.info(f"Updating {offset} to {offset+limit}")
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
f"SELECT * FROM apipayments ORDER BY time LIMIT {limit} OFFSET {offset}"
|
# 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
|
||||||
)
|
)
|
||||||
payments = result.mappings().all()
|
payments = result.mappings().all()
|
||||||
logger.info(f"Payments count: {len(payments)}")
|
logger.info(f"Payments count: {len(payments)}")
|
||||||
@@ -629,11 +640,12 @@ async def m027_update_apipayments_data(db: Connection):
|
|||||||
tag = extra.get("tag")
|
tag = extra.get("tag")
|
||||||
tsph = db.timestamp_placeholder("created_at")
|
tsph = db.timestamp_placeholder("created_at")
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
f"""
|
f"""
|
||||||
UPDATE apipayments
|
UPDATE apipayments
|
||||||
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
|
SET tag = :tag, created_at = {tsph}, updated_at = {tsph}
|
||||||
WHERE checking_id = :checking_id
|
WHERE checking_id = :checking_id
|
||||||
""",
|
""", # noqa: S608
|
||||||
{
|
{
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"created_at": created_at,
|
"created_at": created_at,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from .audit import AuditEntry, AuditFilters
|
from .audit import AuditEntry, AuditFilters
|
||||||
from .lnurl import CreateLnurl, CreateLnurlAuth, PayLnurlWData
|
from .lnurl import CreateLnurlPayment, CreateLnurlWithdraw
|
||||||
from .misc import (
|
from .misc import (
|
||||||
BalanceDelta,
|
BalanceDelta,
|
||||||
Callback,
|
Callback,
|
||||||
@@ -9,6 +9,7 @@ from .misc import (
|
|||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
)
|
)
|
||||||
from .payments import (
|
from .payments import (
|
||||||
|
CancelInvoice,
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreatePayment,
|
CreatePayment,
|
||||||
DecodePayment,
|
DecodePayment,
|
||||||
@@ -23,6 +24,7 @@ from .payments import (
|
|||||||
PaymentsStatusCount,
|
PaymentsStatusCount,
|
||||||
PaymentState,
|
PaymentState,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
|
SettleInvoice,
|
||||||
)
|
)
|
||||||
from .tinyurl import TinyURL
|
from .tinyurl import TinyURL
|
||||||
from .users import (
|
from .users import (
|
||||||
@@ -57,11 +59,12 @@ __all__ = [
|
|||||||
"BalanceDelta",
|
"BalanceDelta",
|
||||||
"BaseWallet",
|
"BaseWallet",
|
||||||
"Callback",
|
"Callback",
|
||||||
|
"CancelInvoice",
|
||||||
"ConversionData",
|
"ConversionData",
|
||||||
"CoreAppExtra",
|
"CoreAppExtra",
|
||||||
"CreateInvoice",
|
"CreateInvoice",
|
||||||
"CreateLnurl",
|
"CreateLnurlPayment",
|
||||||
"CreateLnurlAuth",
|
"CreateLnurlWithdraw",
|
||||||
"CreatePayment",
|
"CreatePayment",
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
"CreateWallet",
|
"CreateWallet",
|
||||||
@@ -72,7 +75,6 @@ __all__ = [
|
|||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
"PayInvoice",
|
"PayInvoice",
|
||||||
"PayLnurlWData",
|
|
||||||
"Payment",
|
"Payment",
|
||||||
"PaymentCountField",
|
"PaymentCountField",
|
||||||
"PaymentCountStat",
|
"PaymentCountStat",
|
||||||
@@ -85,6 +87,7 @@ __all__ = [
|
|||||||
"PaymentsStatusCount",
|
"PaymentsStatusCount",
|
||||||
"RegisterUser",
|
"RegisterUser",
|
||||||
"ResetUserPassword",
|
"ResetUserPassword",
|
||||||
|
"SettleInvoice",
|
||||||
"SimpleStatus",
|
"SimpleStatus",
|
||||||
"TinyURL",
|
"TinyURL",
|
||||||
"UpdateBalance",
|
"UpdateBalance",
|
||||||
|
|||||||
@@ -211,7 +211,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
self, amount: int | None = None
|
self, amount: int | None = None
|
||||||
) -> ReleasePaymentInfo | None:
|
) -> ReleasePaymentInfo | None:
|
||||||
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
||||||
assert url, "Missing URL for payment info."
|
if not url:
|
||||||
|
raise ValueError("Missing URL for payment info.")
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
resp = await client.get(url)
|
resp = await client.get(url)
|
||||||
@@ -376,9 +377,8 @@ class InstallableExtension(BaseModel):
|
|||||||
if ext_zip_file.is_file():
|
if ext_zip_file.is_file():
|
||||||
os.remove(ext_zip_file)
|
os.remove(ext_zip_file)
|
||||||
try:
|
try:
|
||||||
assert (
|
if not self.meta or not self.meta.installed_release:
|
||||||
self.meta and self.meta.installed_release
|
raise ValueError("No installed release.")
|
||||||
), "installed_release is none."
|
|
||||||
|
|
||||||
self._restore_payment_info()
|
self._restore_payment_info()
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from lnurl import LnAddress, Lnurl, LnurlPayResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurl(BaseModel):
|
class CreateLnurlPayment(BaseModel):
|
||||||
description_hash: str
|
res: LnurlPayResponse
|
||||||
callback: str
|
|
||||||
amount: int
|
amount: int
|
||||||
comment: Optional[str] = None
|
comment: Optional[str] = None
|
||||||
description: Optional[str] = None
|
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
|
internal_memo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlAuth(BaseModel):
|
class CreateLnurlWithdraw(BaseModel):
|
||||||
callback: str
|
lnurl_w: Lnurl
|
||||||
|
|
||||||
|
|
||||||
class PayLnurlWData(BaseModel):
|
class LnurlScan(BaseModel):
|
||||||
lnurl_w: str
|
lnurl: Lnurl | LnAddress
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from enum import Enum
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from lnbits.core.models.users import UserNotifications
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(Enum):
|
class NotificationType(Enum):
|
||||||
server_status = "server_status"
|
server_status = "server_status"
|
||||||
@@ -18,6 +20,7 @@ class NotificationType(Enum):
|
|||||||
class NotificationMessage(BaseModel):
|
class NotificationMessage(BaseModel):
|
||||||
message_type: NotificationType
|
message_type: NotificationType
|
||||||
values: dict
|
values: dict
|
||||||
|
user_notifications: UserNotifications | None = None
|
||||||
|
|
||||||
|
|
||||||
NOTIFICATION_TEMPLATES = {
|
NOTIFICATION_TEMPLATES = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from enum import Enum
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
from lnurl import LnurlWithdrawResponse
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
@@ -252,13 +253,25 @@ class CreateInvoice(BaseModel):
|
|||||||
memo: str | None = Query(None, max_length=640)
|
memo: str | None = Query(None, max_length=640)
|
||||||
description_hash: str | None = None
|
description_hash: str | None = None
|
||||||
unhashed_description: str | None = None
|
unhashed_description: str | None = None
|
||||||
|
payment_hash: str | None = Query(
|
||||||
|
None,
|
||||||
|
description="The payment hash of the hold invoice.",
|
||||||
|
min_length=64,
|
||||||
|
max_length=64,
|
||||||
|
)
|
||||||
expiry: int | None = None
|
expiry: int | None = None
|
||||||
extra: dict | None = None
|
extra: dict | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_callback: str | None = None
|
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
|
|
||||||
|
@validator("payment_hash")
|
||||||
|
def check_hex(cls, v):
|
||||||
|
if v:
|
||||||
|
_ = bytes.fromhex(v)
|
||||||
|
return v
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
def unit_is_from_allowed_currencies(cls, v):
|
def unit_is_from_allowed_currencies(cls, v):
|
||||||
@@ -272,3 +285,31 @@ class PaymentsStatusCount(BaseModel):
|
|||||||
outgoing: int = 0
|
outgoing: int = 0
|
||||||
failed: int = 0
|
failed: int = 0
|
||||||
pending: int = 0
|
pending: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class SettleInvoice(BaseModel):
|
||||||
|
preimage: str = Field(
|
||||||
|
...,
|
||||||
|
description="The preimage of the payment hash to settle the invoice.",
|
||||||
|
min_length=64,
|
||||||
|
max_length=64,
|
||||||
|
)
|
||||||
|
|
||||||
|
@validator("preimage")
|
||||||
|
def check_hex(cls, v):
|
||||||
|
_ = bytes.fromhex(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class CancelInvoice(BaseModel):
|
||||||
|
payment_hash: str = Field(
|
||||||
|
...,
|
||||||
|
description="The payment hash of the invoice to cancel.",
|
||||||
|
min_length=64,
|
||||||
|
max_length=64,
|
||||||
|
)
|
||||||
|
|
||||||
|
@validator("payment_hash")
|
||||||
|
def check_hex(cls, v):
|
||||||
|
_ = bytes.fromhex(v)
|
||||||
|
return v
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from bcrypt import checkpw, gensalt, hashpw
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from passlib.context import CryptContext
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleItem
|
from lnbits.core.models.misc import SimpleItem
|
||||||
@@ -20,6 +20,15 @@ from lnbits.settings import settings
|
|||||||
from .wallets import Wallet
|
from .wallets import Wallet
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotifications(BaseModel):
|
||||||
|
nostr_identifier: str | None = None
|
||||||
|
telegram_chat_id: str | None = None
|
||||||
|
email_address: str | None = None
|
||||||
|
excluded_wallets: list[str] = []
|
||||||
|
outgoing_payments_sats: int = 0
|
||||||
|
incoming_payments_sats: int = 0
|
||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: bool | None = False
|
||||||
first_name: str | None = None
|
first_name: str | None = None
|
||||||
@@ -35,6 +44,8 @@ class UserExtra(BaseModel):
|
|||||||
# how many wallets are shown in the user interface
|
# how many wallets are shown in the user interface
|
||||||
visible_wallet_count: int | None = 10
|
visible_wallet_count: int | None = 10
|
||||||
|
|
||||||
|
notifications: UserNotifications = UserNotifications()
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
@@ -120,16 +131,18 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
def hash_password(self, password: str) -> str:
|
||||||
"""sets and returns the hashed password"""
|
"""sets and returns the hashed password"""
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
salt = gensalt()
|
||||||
self.password_hash = pwd_context.hash(password)
|
hashed_pw = hashpw(password.encode(), salt)
|
||||||
|
if not hashed_pw:
|
||||||
|
raise ValueError("Password hashing failed.")
|
||||||
|
self.password_hash = hashed_pw.decode()
|
||||||
return self.password_hash
|
return self.password_hash
|
||||||
|
|
||||||
def verify_password(self, password: str) -> bool:
|
def verify_password(self, password: str) -> bool:
|
||||||
"""returns True if the password matches the hash"""
|
"""returns True if the password matches the hash"""
|
||||||
if not self.password_hash:
|
if not self.password_hash:
|
||||||
return False
|
return False
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
return checkpw(password.encode(), self.password_hash.encode())
|
||||||
return pwd_context.verify(password, self.password_hash)
|
|
||||||
|
|
||||||
def validate_fields(self):
|
def validate_fields(self):
|
||||||
if self.username and not is_valid_username(self.username):
|
if self.username and not is_valid_username(self.username):
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from datetime import datetime, timezone
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey
|
from ecdsa import SECP256k1, SigningKey
|
||||||
|
from lnurl import encode as lnurl_encode
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.lnurl import encode as lnurl_encode
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,23 @@ from .funding_source import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl
|
||||||
from .notifications import enqueue_notification, send_payment_notification
|
from .notifications import enqueue_admin_notification, send_payment_notification
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
|
cancel_hold_invoice,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
create_fiat_invoice,
|
||||||
create_invoice,
|
create_invoice,
|
||||||
|
create_payment_request,
|
||||||
create_wallet_invoice,
|
create_wallet_invoice,
|
||||||
fee_reserve,
|
fee_reserve,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
get_payments_daily_stats,
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
service_fee,
|
service_fee,
|
||||||
|
settle_hold_invoice,
|
||||||
update_pending_payment,
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -35,25 +38,28 @@ from .websockets import websocket_manager, websocket_updater
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
|
"cancel_hold_invoice",
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"check_webpush_settings",
|
"check_webpush_settings",
|
||||||
"create_fiat_invoice",
|
"create_fiat_invoice",
|
||||||
"create_invoice",
|
"create_invoice",
|
||||||
|
"create_payment_request",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"create_wallet_invoice",
|
"create_wallet_invoice",
|
||||||
"enqueue_notification",
|
"enqueue_admin_notification",
|
||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
|
"fetch_lnurl_pay_request",
|
||||||
"get_balance_delta",
|
"get_balance_delta",
|
||||||
"get_payments_daily_stats",
|
"get_payments_daily_stats",
|
||||||
|
"get_pr_from_lnurl",
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"perform_lnurlauth",
|
|
||||||
"redeem_lnurl_withdraw",
|
|
||||||
"send_payment_notification",
|
"send_payment_notification",
|
||||||
"service_fee",
|
"service_fee",
|
||||||
|
"settle_hold_invoice",
|
||||||
"switch_to_voidwallet",
|
"switch_to_voidwallet",
|
||||||
"update_cached_settings",
|
"update_cached_settings",
|
||||||
"update_pending_payment",
|
"update_pending_payment",
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ async def install_extension(ext_info: InstallableExtension) -> Extension:
|
|||||||
|
|
||||||
ext_info.meta = ext_info.meta or ExtensionMeta()
|
ext_info.meta = ext_info.meta or ExtensionMeta()
|
||||||
|
|
||||||
if ext_info.meta.installed_release:
|
if (
|
||||||
assert (
|
ext_info.meta.installed_release
|
||||||
ext_info.meta.installed_release.is_version_compatible
|
and not ext_info.meta.installed_release.is_version_compatible
|
||||||
), "Incompatible extension version"
|
):
|
||||||
|
raise ValueError("Incompatible extension version")
|
||||||
|
|
||||||
installed_ext = await get_installed_extension(ext_info.id)
|
installed_ext = await get_installed_extension(ext_info.id)
|
||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
@@ -93,9 +94,8 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
|||||||
old_module = importlib.import_module(ext.module_name)
|
old_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
stop_fn_name = f"{ext_id}_stop"
|
stop_fn_name = f"{ext_id}_stop"
|
||||||
assert hasattr(
|
if not hasattr(old_module, stop_fn_name):
|
||||||
old_module, stop_fn_name
|
raise ValueError(f"No stop function found for '{ext.module_name}'.")
|
||||||
), f"No stop function found for '{ext.module_name}'."
|
|
||||||
|
|
||||||
stop_fn = getattr(old_module, stop_fn_name)
|
stop_fn = getattr(old_module, stop_fn_name)
|
||||||
if stop_fn:
|
if stop_fn:
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ async def test_connection(provider: str) -> SimpleStatus:
|
|||||||
This function should be called when setting up or testing the Stripe integration.
|
This function should be called when setting up or testing the Stripe integration.
|
||||||
"""
|
"""
|
||||||
fiat_provider = await get_fiat_provider(provider)
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
|
if not fiat_provider:
|
||||||
|
return SimpleStatus(
|
||||||
|
success=False,
|
||||||
|
message=f"Fiat provider '{provider}' not found.",
|
||||||
|
)
|
||||||
status = await fiat_provider.status()
|
status = await fiat_provider.status()
|
||||||
if status.error_message:
|
if status.error_message:
|
||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
from lnbits.core.services.notifications import enqueue_admin_notification
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets import get_funding_source, set_funding_source
|
from lnbits.wallets import get_funding_source, set_funding_source
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ async def check_server_balance_against_node():
|
|||||||
f"Balance delta reached: {status.delta_sats} sats."
|
f"Balance delta reached: {status.delta_sats} sats."
|
||||||
f" Switch to void wallet: {use_voidwallet}."
|
f" Switch to void wallet: {use_voidwallet}."
|
||||||
)
|
)
|
||||||
enqueue_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.watchdog_check,
|
NotificationType.watchdog_check,
|
||||||
{
|
{
|
||||||
"delta_sats": status.delta_sats,
|
"delta_sats": status.delta_sats,
|
||||||
@@ -71,7 +71,7 @@ async def check_balance_delta_changed():
|
|||||||
settings.latest_balance_delta_sats = status.delta_sats
|
settings.latest_balance_delta_sats = status.delta_sats
|
||||||
return
|
return
|
||||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||||
enqueue_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.balance_delta,
|
NotificationType.balance_delta,
|
||||||
{
|
{
|
||||||
"delta_sats": status.delta_sats,
|
"delta_sats": status.delta_sats,
|
||||||
|
|||||||
+48
-148
@@ -1,158 +1,58 @@
|
|||||||
import asyncio
|
from lnurl import (
|
||||||
import json
|
LnurlErrorResponse,
|
||||||
from io import BytesIO
|
LnurlPayActionResponse,
|
||||||
from typing import Optional
|
LnurlPayResponse,
|
||||||
from urllib.parse import parse_qs, urlparse
|
LnurlResponseException,
|
||||||
|
execute_pay_request,
|
||||||
import httpx
|
handle,
|
||||||
from fastapi import Depends
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.db import Connection
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
require_admin_key,
|
|
||||||
)
|
)
|
||||||
from lnbits.helpers import check_callback_url, url_for
|
|
||||||
from lnbits.lnurl import LnurlErrorResponse
|
from lnbits.core.models import CreateLnurlPayment
|
||||||
from lnbits.lnurl import decode as decode_lnurl
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
from .payments import create_invoice
|
|
||||||
|
|
||||||
|
|
||||||
async def redeem_lnurl_withdraw(
|
async def get_pr_from_lnurl(
|
||||||
wallet_id: str,
|
lnurl: str, amount_msat: int, comment: str | None = None
|
||||||
lnurl_request: str,
|
) -> str:
|
||||||
memo: Optional[str] = None,
|
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
|
||||||
extra: Optional[dict] = None,
|
if isinstance(res, LnurlErrorResponse):
|
||||||
wait_seconds: int = 0,
|
raise LnurlResponseException(res.reason)
|
||||||
conn: Optional[Connection] = None,
|
if not isinstance(res, LnurlPayResponse):
|
||||||
) -> None:
|
raise LnurlResponseException(
|
||||||
if not lnurl_request:
|
"Invalid LNURL response. Expected LnurlPayResponse."
|
||||||
return None
|
|
||||||
|
|
||||||
res = {}
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
lnurl = decode_lnurl(lnurl_request)
|
|
||||||
check_callback_url(str(lnurl))
|
|
||||||
r = await client.get(str(lnurl))
|
|
||||||
res = r.json()
|
|
||||||
|
|
||||||
try:
|
|
||||||
_, payment_request = await create_invoice(
|
|
||||||
wallet_id=wallet_id,
|
|
||||||
amount=int(res["maxWithdrawable"] / 1000),
|
|
||||||
memo=memo or res["defaultDescription"] or "",
|
|
||||||
extra=extra,
|
|
||||||
conn=conn,
|
|
||||||
)
|
)
|
||||||
except Exception:
|
res2 = await execute_pay_request(
|
||||||
logger.warning(
|
res,
|
||||||
f"failed to create invoice on redeem_lnurl_withdraw "
|
msat=amount_msat,
|
||||||
f"from {lnurl}. params: {res}"
|
comment=comment,
|
||||||
)
|
user_agent=settings.user_agent,
|
||||||
return None
|
timeout=10,
|
||||||
|
)
|
||||||
if wait_seconds:
|
if isinstance(res2, LnurlErrorResponse):
|
||||||
await asyncio.sleep(wait_seconds)
|
raise LnurlResponseException(res2.reason)
|
||||||
|
return res2.pr
|
||||||
params = {"k1": res["k1"], "pr": payment_request}
|
|
||||||
|
|
||||||
try:
|
|
||||||
params["balanceNotify"] = url_for(
|
|
||||||
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
|
||||||
external=True,
|
|
||||||
wal=wallet_id,
|
|
||||||
)
|
|
||||||
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:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def perform_lnurlauth(
|
async def fetch_lnurl_pay_request(data: CreateLnurlPayment) -> LnurlPayActionResponse:
|
||||||
callback: str,
|
"""
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
Pay an LNURL payment request.
|
||||||
) -> Optional[LnurlErrorResponse]:
|
|
||||||
cb = urlparse(callback)
|
|
||||||
|
|
||||||
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
raises `LnurlResponseException` if pay request fails
|
||||||
|
"""
|
||||||
|
|
||||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
if data.unit and data.unit != "sat":
|
||||||
|
# shift to float with 2 decimal places
|
||||||
|
amount = round(data.amount / 1000, 2)
|
||||||
|
amount_msat = await fiat_amount_as_satoshis(amount, data.unit)
|
||||||
|
amount_msat *= 1000
|
||||||
|
else:
|
||||||
|
amount_msat = data.amount
|
||||||
|
|
||||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
return await execute_pay_request(
|
||||||
"""for strict DER we need to encode the integer with some quirks"""
|
data.res,
|
||||||
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
|
msat=amount_msat,
|
||||||
|
comment=data.comment,
|
||||||
if len(b) == 0:
|
user_agent=settings.user_agent,
|
||||||
# ensure there's at least one byte when the int is zero
|
timeout=10,
|
||||||
return bytes([0])
|
)
|
||||||
|
|
||||||
if b[0] & 0x80 != 0:
|
|
||||||
# ensure it doesn't start with a 0x80 and so it isn't
|
|
||||||
# interpreted as a negative number
|
|
||||||
return bytes([0]) + b
|
|
||||||
|
|
||||||
return b
|
|
||||||
|
|
||||||
def encode_strict_der(r: int, s: int, order: int):
|
|
||||||
# if s > order/2 verification will fail sometimes
|
|
||||||
# so we must fix it here see:
|
|
||||||
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
|
|
||||||
if s > order // 2:
|
|
||||||
s = order - s
|
|
||||||
|
|
||||||
# now we do the strict DER encoding copied from
|
|
||||||
# https://github.com/KiriKiri/bip66 (without any checks)
|
|
||||||
r_temp = int_to_bytes_suitable_der(r)
|
|
||||||
s_temp = int_to_bytes_suitable_der(s)
|
|
||||||
|
|
||||||
r_len = len(r_temp)
|
|
||||||
s_len = len(s_temp)
|
|
||||||
sign_len = 6 + r_len + s_len
|
|
||||||
|
|
||||||
signature = BytesIO()
|
|
||||||
signature.write(0x30.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(r_len.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(r_temp)
|
|
||||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(s_len.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(s_temp)
|
|
||||||
|
|
||||||
return signature.getvalue()
|
|
||||||
|
|
||||||
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
|
||||||
check_callback_url(callback)
|
|
||||||
r = await client.get(
|
|
||||||
callback,
|
|
||||||
params={
|
|
||||||
"k1": k1.hex(),
|
|
||||||
"key": key.verifying_key.to_string("compressed").hex(),
|
|
||||||
"sig": sig.hex(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
resp = json.loads(r.text)
|
|
||||||
if resp["status"] == "OK":
|
|
||||||
return None
|
|
||||||
|
|
||||||
return LnurlErrorResponse(reason=resp["reason"])
|
|
||||||
except (KeyError, json.decoder.JSONDecodeError):
|
|
||||||
return LnurlErrorResponse(
|
|
||||||
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -16,23 +16,25 @@ from lnbits.core.crud import (
|
|||||||
get_webpush_subscriptions_for_user,
|
get_webpush_subscriptions_for_user,
|
||||||
mark_webhook_sent,
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.crud.users import get_user
|
||||||
from lnbits.core.models import Payment, Wallet
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
NotificationMessage,
|
NotificationMessage,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.models.users import UserNotifications
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||||
from lnbits.core.services.websockets import websocket_manager
|
from lnbits.core.services.websockets import websocket_manager
|
||||||
from lnbits.helpers import check_callback_url, is_valid_email_address
|
from lnbits.helpers import check_callback_url, is_valid_email_address
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
from lnbits.utils.nostr import normalize_private_key
|
||||||
|
|
||||||
notifications_queue: asyncio.Queue = asyncio.Queue()
|
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
|
def enqueue_admin_notification(message_type: NotificationType, values: dict) -> None:
|
||||||
if not is_message_type_enabled(message_type):
|
if not _is_message_type_enabled(message_type):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
notifications_queue.put_nowait(
|
notifications_queue.put_nowait(
|
||||||
@@ -42,62 +44,125 @@ def enqueue_notification(message_type: NotificationType, values: dict) -> None:
|
|||||||
logger.error(f"Error enqueuing notification: {e}")
|
logger.error(f"Error enqueuing notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def process_next_notification():
|
def enqueue_user_notification(
|
||||||
notification_message: NotificationMessage = await notifications_queue.get()
|
message_type: NotificationType, values: dict, user_notifications: UserNotifications
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
notifications_queue.put_nowait(
|
||||||
|
NotificationMessage(
|
||||||
|
message_type=message_type,
|
||||||
|
values=values,
|
||||||
|
user_notifications=user_notifications,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error enqueuing notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def process_next_notification() -> None:
|
||||||
|
notification_message = await notifications_queue.get()
|
||||||
message_type, text = _notification_message_to_text(notification_message)
|
message_type, text = _notification_message_to_text(notification_message)
|
||||||
await send_notification(text, message_type)
|
user_notifications = notification_message.user_notifications
|
||||||
|
if user_notifications:
|
||||||
|
await send_user_notification(user_notifications, text, message_type)
|
||||||
|
else:
|
||||||
|
await send_admin_notification(text, message_type)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_admin_notification(
|
||||||
|
message: str,
|
||||||
|
message_type: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
return await send_notification(
|
||||||
|
settings.lnbits_telegram_notifications_chat_id,
|
||||||
|
settings.lnbits_nostr_notifications_identifiers,
|
||||||
|
settings.lnbits_email_notifications_to_emails,
|
||||||
|
message,
|
||||||
|
message_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_user_notification(
|
||||||
|
user_notifications: UserNotifications,
|
||||||
|
message: str,
|
||||||
|
message_type: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
email_address = (
|
||||||
|
[user_notifications.email_address] if user_notifications.email_address else []
|
||||||
|
)
|
||||||
|
nostr_identifiers = (
|
||||||
|
[user_notifications.nostr_identifier]
|
||||||
|
if user_notifications.nostr_identifier
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
return await send_notification(
|
||||||
|
user_notifications.telegram_chat_id,
|
||||||
|
nostr_identifiers,
|
||||||
|
email_address,
|
||||||
|
message,
|
||||||
|
message_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_notification(
|
async def send_notification(
|
||||||
|
telegram_chat_id: str | None,
|
||||||
|
nostr_identifiers: list[str] | None,
|
||||||
|
email_addresses: list[str] | None,
|
||||||
message: str,
|
message: str,
|
||||||
message_type: Optional[str] = None,
|
message_type: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
if settings.lnbits_telegram_notifications_enabled:
|
if telegram_chat_id and settings.is_telegram_notifications_configured():
|
||||||
await send_telegram_notification(message)
|
await send_telegram_notification(telegram_chat_id, message)
|
||||||
logger.debug(f"Sent telegram notification: {message_type}")
|
logger.debug(f"Sent telegram notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if settings.lnbits_nostr_notifications_enabled:
|
if nostr_identifiers and settings.is_nostr_notifications_configured():
|
||||||
await send_nostr_notification(message)
|
await send_nostr_notifications(nostr_identifiers, message)
|
||||||
logger.debug(f"Sent nostr notification: {message_type}")
|
logger.debug(f"Sent nostr notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
||||||
try:
|
try:
|
||||||
if settings.lnbits_email_notifications_enabled:
|
if email_addresses and settings.lnbits_email_notifications_enabled:
|
||||||
await send_email_notification(message)
|
await send_email_notification(email_addresses, message)
|
||||||
logger.debug(f"Sent email notification: {message_type}")
|
logger.debug(f"Sent email notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending email notification {message_type}: {e}")
|
logger.error(f"Error sending email notification {message_type}: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_notification(message: str) -> dict:
|
async def send_nostr_notifications(identifiers: list[str], message: str) -> list[str]:
|
||||||
for i in settings.lnbits_nostr_notifications_identifiers:
|
success_sent: list[str] = []
|
||||||
|
for identifier in identifiers:
|
||||||
try:
|
try:
|
||||||
identifier = await fetch_nip5_details(i)
|
await send_nostr_notification(identifier, message)
|
||||||
user_pubkey = identifier[0]
|
success_sent.append(identifier)
|
||||||
relays = identifier[1]
|
|
||||||
server_private_key = normalize_private_key(
|
|
||||||
settings.lnbits_nostr_notifications_private_key
|
|
||||||
)
|
|
||||||
await send_nostr_dm(
|
|
||||||
server_private_key,
|
|
||||||
user_pubkey,
|
|
||||||
message,
|
|
||||||
relays,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error notifying identifier {i}: {e}")
|
logger.warning(f"Error notifying identifier {identifier}: {e}")
|
||||||
|
return success_sent
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
async def send_telegram_notification(message: str) -> dict:
|
async def send_nostr_notification(identifier: str, message: str):
|
||||||
|
nip5 = await fetch_nip5_details(identifier)
|
||||||
|
user_pubkey = nip5[0]
|
||||||
|
relays = nip5[1]
|
||||||
|
server_private_key = normalize_private_key(
|
||||||
|
settings.lnbits_nostr_notifications_private_key
|
||||||
|
)
|
||||||
|
await send_nostr_dm(
|
||||||
|
server_private_key,
|
||||||
|
user_pubkey,
|
||||||
|
message,
|
||||||
|
relays,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_telegram_notification(chat_id: str, message: str) -> dict:
|
||||||
return await send_telegram_message(
|
return await send_telegram_message(
|
||||||
settings.lnbits_telegram_notifications_access_token,
|
settings.lnbits_telegram_notifications_access_token,
|
||||||
settings.lnbits_telegram_notifications_chat_id,
|
chat_id,
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,7 +177,7 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def send_email_notification(
|
async def send_email_notification(
|
||||||
message: str, subject: str = "LNbits Notification"
|
to_emails: list[str], message: str, subject: str = "LNbits Notification"
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if not settings.lnbits_email_notifications_enabled:
|
if not settings.lnbits_email_notifications_enabled:
|
||||||
return {"status": "error", "message": "Email notifications are disabled"}
|
return {"status": "error", "message": "Email notifications are disabled"}
|
||||||
@@ -123,7 +188,7 @@ async def send_email_notification(
|
|||||||
settings.lnbits_email_notifications_username,
|
settings.lnbits_email_notifications_username,
|
||||||
settings.lnbits_email_notifications_password,
|
settings.lnbits_email_notifications_password,
|
||||||
settings.lnbits_email_notifications_email,
|
settings.lnbits_email_notifications_email,
|
||||||
settings.lnbits_email_notifications_to_emails,
|
to_emails,
|
||||||
subject,
|
subject,
|
||||||
message,
|
message,
|
||||||
)
|
)
|
||||||
@@ -163,41 +228,6 @@ async def send_email(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
|
||||||
if message_type == NotificationType.balance_update:
|
|
||||||
return settings.lnbits_notification_credit_debit
|
|
||||||
if message_type == NotificationType.settings_update:
|
|
||||||
return settings.lnbits_notification_settings_update
|
|
||||||
if message_type == NotificationType.watchdog_check:
|
|
||||||
return settings.lnbits_notification_watchdog
|
|
||||||
if message_type == NotificationType.balance_delta:
|
|
||||||
return settings.notification_balance_delta_changed
|
|
||||||
if message_type == NotificationType.server_start_stop:
|
|
||||||
return settings.lnbits_notification_server_start_stop
|
|
||||||
if message_type == NotificationType.server_status:
|
|
||||||
return settings.lnbits_notification_server_status_hours > 0
|
|
||||||
if message_type == NotificationType.incoming_payment:
|
|
||||||
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
|
||||||
if message_type == NotificationType.outgoing_payment:
|
|
||||||
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _notification_message_to_text(
|
|
||||||
notification_message: NotificationMessage,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
message_type = notification_message.message_type.value
|
|
||||||
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
|
||||||
try:
|
|
||||||
text = meesage_value.format(**notification_message.values)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error formatting notification message: {e}")
|
|
||||||
text = meesage_value
|
|
||||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
|
||||||
return message_type, text
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
async def dispatch_webhook(payment: Payment):
|
||||||
"""
|
"""
|
||||||
Dispatches the webhook to the webhook url.
|
Dispatches the webhook to the webhook url.
|
||||||
@@ -231,7 +261,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
send_chat_payment_notification(wallet, payment)
|
await send_chat_payment_notification(wallet, payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending chat payment notification {e!s}")
|
logger.error(f"Error sending chat payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
@@ -268,7 +298,7 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
amount_sats = abs(payment.sat)
|
amount_sats = abs(payment.sat)
|
||||||
values: dict = {
|
values: dict = {
|
||||||
"wallet_id": wallet.id,
|
"wallet_id": wallet.id,
|
||||||
@@ -283,10 +313,23 @@ def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
|
|
||||||
if payment.is_out:
|
if payment.is_out:
|
||||||
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
||||||
enqueue_notification(NotificationType.outgoing_payment, values)
|
enqueue_admin_notification(NotificationType.outgoing_payment, values)
|
||||||
else:
|
elif amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
||||||
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
enqueue_admin_notification(NotificationType.incoming_payment, values)
|
||||||
enqueue_notification(NotificationType.incoming_payment, values)
|
|
||||||
|
user = await get_user(wallet.user)
|
||||||
|
user_notifications = user.extra.notifications if user else None
|
||||||
|
if user_notifications and wallet.id not in user_notifications.excluded_wallets:
|
||||||
|
out_limit = user_notifications.outgoing_payments_sats
|
||||||
|
in_limit = user_notifications.incoming_payments_sats
|
||||||
|
if payment.is_out and (amount_sats >= out_limit):
|
||||||
|
enqueue_user_notification(
|
||||||
|
NotificationType.outgoing_payment, values, user_notifications
|
||||||
|
)
|
||||||
|
elif amount_sats >= in_limit:
|
||||||
|
enqueue_user_notification(
|
||||||
|
NotificationType.incoming_payment, values, user_notifications
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
||||||
@@ -330,3 +373,38 @@ async def send_push_notification(subscription, title, body, url=""):
|
|||||||
f"failed sending push notification: "
|
f"failed sending push notification: "
|
||||||
f"{e.response.text if e.response else e}"
|
f"{e.response.text if e.response else e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
|
if message_type == NotificationType.balance_update:
|
||||||
|
return settings.lnbits_notification_credit_debit
|
||||||
|
if message_type == NotificationType.settings_update:
|
||||||
|
return settings.lnbits_notification_settings_update
|
||||||
|
if message_type == NotificationType.watchdog_check:
|
||||||
|
return settings.lnbits_notification_watchdog
|
||||||
|
if message_type == NotificationType.balance_delta:
|
||||||
|
return settings.notification_balance_delta_changed
|
||||||
|
if message_type == NotificationType.server_start_stop:
|
||||||
|
return settings.lnbits_notification_server_start_stop
|
||||||
|
if message_type == NotificationType.server_status:
|
||||||
|
return settings.lnbits_notification_server_status_hours > 0
|
||||||
|
if message_type == NotificationType.incoming_payment:
|
||||||
|
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
||||||
|
if message_type == NotificationType.outgoing_payment:
|
||||||
|
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _notification_message_to_text(
|
||||||
|
notification_message: NotificationMessage,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
message_type = notification_message.message_type.value
|
||||||
|
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
||||||
|
try:
|
||||||
|
text = meesage_value.format(**notification_message.values)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error formatting notification message: {e}")
|
||||||
|
text = meesage_value
|
||||||
|
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
||||||
|
return message_type, text
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11 import encode as bolt11_encode
|
from bolt11 import encode as bolt11_encode
|
||||||
|
from lnurl import LnurlErrorResponse, LnurlSuccessResponse
|
||||||
|
from lnurl import execute_withdraw as lnurl_withdraw
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
@@ -16,15 +16,16 @@ from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
|||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
from lnbits.tasks import create_task, internal_invoice_queue_put
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash
|
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
|
InvoiceResponse,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -66,7 +67,8 @@ async def pay_invoice(
|
|||||||
if settings.lnbits_only_allow_incoming_payments:
|
if settings.lnbits_only_allow_incoming_payments:
|
||||||
raise PaymentError("Only incoming payments allowed.", status="failed")
|
raise PaymentError("Only incoming payments allowed.", status="failed")
|
||||||
invoice = _validate_payment_request(payment_request, max_sat)
|
invoice = _validate_payment_request(payment_request, max_sat)
|
||||||
assert invoice.amount_msat
|
if not invoice.amount_msat:
|
||||||
|
raise ValueError("Missig invoice amount.")
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
amount_msat = invoice.amount_msat
|
amount_msat = invoice.amount_msat
|
||||||
@@ -89,16 +91,24 @@ async def pay_invoice(
|
|||||||
|
|
||||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||||
|
|
||||||
service_fee_memo = f"""
|
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
|
||||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(payment, service_fee_memo, new_conn)
|
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
||||||
|
|
||||||
return payment
|
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(
|
async def create_fiat_invoice(
|
||||||
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
|
wallet_id: str, invoice_data: CreateInvoice, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
@@ -122,6 +132,9 @@ async def create_fiat_invoice(
|
|||||||
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
|
internal_payment = await create_wallet_invoice(wallet_id, invoice_data)
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(fiat_provider_name)
|
fiat_provider = await get_fiat_provider(fiat_provider_name)
|
||||||
|
if not fiat_provider:
|
||||||
|
raise InvoiceError("No fiat provider found.", status="failed")
|
||||||
|
|
||||||
fiat_invoice = await fiat_provider.create_invoice(
|
fiat_invoice = await fiat_provider.create_invoice(
|
||||||
amount=invoice_data.amount,
|
amount=invoice_data.amount,
|
||||||
payment_hash=internal_payment.payment_hash,
|
payment_hash=internal_payment.payment_hash,
|
||||||
@@ -155,8 +168,8 @@ async def create_fiat_invoice(
|
|||||||
|
|
||||||
|
|
||||||
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||||
description_hash = b""
|
description_hash = None
|
||||||
unhashed_description = b""
|
unhashed_description = None
|
||||||
memo = data.memo or settings.lnbits_site_title
|
memo = data.memo or settings.lnbits_site_title
|
||||||
if data.description_hash or data.unhashed_description:
|
if data.description_hash or data.unhashed_description:
|
||||||
if data.description_hash:
|
if data.description_hash:
|
||||||
@@ -187,30 +200,29 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
|
payment_hash=data.payment_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
# lnurl_response is not saved in the database
|
if data.lnurl_withdraw:
|
||||||
if data.lnurl_callback:
|
try:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
check_callback_url(data.lnurl_withdraw.callback)
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
res = await lnurl_withdraw(
|
||||||
try:
|
data.lnurl_withdraw,
|
||||||
check_callback_url(data.lnurl_callback)
|
payment.bolt11,
|
||||||
r = await client.get(
|
user_agent=settings.user_agent,
|
||||||
data.lnurl_callback,
|
timeout=10,
|
||||||
params={"pr": payment.bolt11},
|
)
|
||||||
timeout=10,
|
if isinstance(res, LnurlErrorResponse):
|
||||||
)
|
payment.extra["lnurl_response"] = res.reason
|
||||||
if r.is_error:
|
payment.status = "failed"
|
||||||
payment.extra["lnurl_response"] = r.text
|
elif isinstance(res, LnurlSuccessResponse):
|
||||||
else:
|
payment.extra["lnurl_response"] = True
|
||||||
resp = json.loads(r.text)
|
payment.status = "success"
|
||||||
if resp["status"] != "OK":
|
except Exception as exc:
|
||||||
payment.extra["lnurl_response"] = resp["reason"]
|
payment.extra["lnurl_response"] = str(exc)
|
||||||
else:
|
payment.status = "failed"
|
||||||
payment.extra["lnurl_response"] = True
|
# updating to payment here would run into a race condition
|
||||||
except (httpx.ConnectError, httpx.RequestError) as ex:
|
# with the payment listeners and they will overwrite each other
|
||||||
logger.error(ex)
|
|
||||||
payment.extra["lnurl_response"] = False
|
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -227,6 +239,7 @@ async def create_invoice(
|
|||||||
extra: Optional[dict] = None,
|
extra: Optional[dict] = None,
|
||||||
webhook: Optional[str] = None,
|
webhook: Optional[str] = None,
|
||||||
internal: Optional[bool] = False,
|
internal: Optional[bool] = False,
|
||||||
|
payment_hash: str | None = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
@@ -260,38 +273,54 @@ async def create_invoice(
|
|||||||
status="failed",
|
status="failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_response = await funding_source.create_invoice(
|
if payment_hash:
|
||||||
amount=amount_sat,
|
try:
|
||||||
memo=invoice_memo,
|
invoice_response = await funding_source.create_hold_invoice(
|
||||||
description_hash=description_hash,
|
amount=amount_sat,
|
||||||
unhashed_description=unhashed_description,
|
memo=invoice_memo,
|
||||||
expiry=expiry or settings.lightning_invoice_expiry,
|
payment_hash=payment_hash,
|
||||||
)
|
description_hash=description_hash,
|
||||||
|
)
|
||||||
|
extra["hold_invoice"] = True
|
||||||
|
except UnsupportedError as exc:
|
||||||
|
raise InvoiceError(
|
||||||
|
"Hold invoices are not supported by the funding source.",
|
||||||
|
status="failed",
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
|
invoice_response = await funding_source.create_invoice(
|
||||||
|
amount=amount_sat,
|
||||||
|
memo=invoice_memo,
|
||||||
|
description_hash=description_hash,
|
||||||
|
unhashed_description=unhashed_description,
|
||||||
|
expiry=expiry or settings.lightning_invoice_expiry,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not payment_response.ok
|
not invoice_response.ok
|
||||||
or not payment_response.payment_request
|
or not invoice_response.payment_request
|
||||||
or not payment_response.checking_id
|
or not invoice_response.checking_id
|
||||||
):
|
):
|
||||||
raise InvoiceError(
|
raise InvoiceError(
|
||||||
message=payment_response.error_message or "unexpected backend error.",
|
message=invoice_response.error_message or "unexpected backend error.",
|
||||||
status="pending",
|
status="pending",
|
||||||
)
|
)
|
||||||
invoice = bolt11_decode(payment_response.payment_request)
|
invoice = bolt11_decode(invoice_response.payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=payment_response.payment_request,
|
bolt11=invoice_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=payment_response.preimage,
|
preimage=invoice_response.preimage,
|
||||||
amount_msat=amount_sat * 1000,
|
amount_msat=amount_sat * 1000,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
|
fee=invoice_response.fee_msat or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
checking_id=payment_response.checking_id,
|
checking_id=invoice_response.checking_id,
|
||||||
data=create_payment_model,
|
data=create_payment_model,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
@@ -309,17 +338,45 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
await update_pending_payment(payment)
|
await update_pending_payment(payment)
|
||||||
|
|
||||||
|
|
||||||
async def update_pending_payment(payment: Payment) -> bool:
|
async def update_pending_payment(payment: Payment) -> Payment:
|
||||||
status = await payment.check_status()
|
status = await payment.check_status()
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment)
|
await update_payment(payment)
|
||||||
return True
|
elif status.success:
|
||||||
if status.success:
|
payment = await update_payment_success_status(payment, status)
|
||||||
payment.status = PaymentState.SUCCESS
|
return payment
|
||||||
await update_payment(payment)
|
|
||||||
return True
|
|
||||||
return False
|
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)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||||
@@ -709,19 +766,20 @@ async def _pay_external_invoice(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
||||||
service_fee_msat = service_fee(amount_msat, internal=False)
|
|
||||||
|
|
||||||
task = create_task(
|
task = create_task(
|
||||||
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
||||||
)
|
)
|
||||||
|
|
||||||
# make sure a hold invoice or deferred payment is not blocking the server
|
# 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:
|
try:
|
||||||
wait_time = max(1, settings.lnbits_funding_source_pay_invoice_wait_seconds)
|
payment_response = await asyncio.wait_for(task, timeout=wait_time)
|
||||||
payment_response = await asyncio.wait_for(task, wait_time)
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
# return pending payment on timeout
|
# return pending payment on timeout
|
||||||
logger.debug(f"payment timeout, {checking_id} is still pending")
|
logger.debug(
|
||||||
|
f"payment timeout after {wait_time}s, {checking_id} is still pending"
|
||||||
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
# payment failed
|
# payment failed
|
||||||
@@ -735,18 +793,28 @@ async def _pay_external_invoice(
|
|||||||
message = payment_response.error_message or "without an error message."
|
message = payment_response.error_message or "without an error message."
|
||||||
raise PaymentError(f"Payment failed: {message}", status="failed")
|
raise PaymentError(f"Payment failed: {message}", status="failed")
|
||||||
|
|
||||||
# payment.ok can be True (paid) or None (pending)!
|
if payment_response.success:
|
||||||
payment.status = (
|
payment = await update_payment_success_status(
|
||||||
PaymentState.SUCCESS if payment_response.ok is True else PaymentState.PENDING
|
payment, payment_response, conn=conn
|
||||||
)
|
)
|
||||||
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)
|
await send_payment_notification(wallet, payment)
|
||||||
logger.success(f"payment successful {payment_response.checking_id}")
|
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
|
return payment
|
||||||
|
|
||||||
|
|
||||||
@@ -780,8 +848,7 @@ async def _verify_external_payment(
|
|||||||
|
|
||||||
if status.success:
|
if status.success:
|
||||||
# payment was successful on the fundingsource
|
# payment was successful on the fundingsource
|
||||||
payment.status = PaymentState.SUCCESS
|
await update_payment_success_status(payment, status, conn=conn)
|
||||||
await update_payment(payment, conn=conn)
|
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
"Failed payment was already paid on the fundingsource.",
|
"Failed payment was already paid on the fundingsource.",
|
||||||
status="success",
|
status="success",
|
||||||
@@ -834,12 +901,16 @@ def _validate_payment_request(
|
|||||||
|
|
||||||
|
|
||||||
async def _credit_service_fee_wallet(
|
async def _credit_service_fee_wallet(
|
||||||
payment: Payment, memo: str, conn: Optional[Connection] = None
|
wallet: Wallet, payment: Payment, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
|
||||||
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
if not settings.lnbits_service_fee_wallet or not service_fee_msat:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
memo = f"""
|
||||||
|
Service fee for payment of {abs(payment.sat)} sats.
|
||||||
|
Wallet: '{wallet.name}' ({wallet.id})."""
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=settings.lnbits_service_fee_wallet,
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
bolt11=payment.bolt11,
|
bolt11=payment.bolt11,
|
||||||
@@ -897,3 +968,40 @@ async def _check_fiat_invoice_limits(
|
|||||||
f"The amount exceeds the '{fiat_provider_name}'"
|
f"The amount exceeds the '{fiat_provider_name}'"
|
||||||
"faucet wallet balance.",
|
"faucet wallet balance.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def settle_hold_invoice(payment: Payment, preimage: str) -> InvoiceResponse:
|
||||||
|
if verify_preimage(preimage, payment.payment_hash) is False:
|
||||||
|
raise InvoiceError("Invalid preimage.", status="failed")
|
||||||
|
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
response = await funding_source.settle_hold_invoice(preimage=preimage)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
raise InvoiceError(
|
||||||
|
response.error_message or "Unexpected backend error.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
payment.preimage = preimage
|
||||||
|
payment.extra["hold_invoice_settled"] = True
|
||||||
|
await update_payment(payment)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
response = await funding_source.cancel_hold_invoice(
|
||||||
|
payment_hash=payment.payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
raise InvoiceError(
|
||||||
|
response.error_message or "Unexpected backend error.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
payment.status = PaymentState.FAILED
|
||||||
|
payment.extra["hold_invoice_cancelled"] = True
|
||||||
|
await update_payment(payment)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ async def check_webpush_settings():
|
|||||||
vapid = Vapid()
|
vapid = Vapid()
|
||||||
vapid.generate_keys()
|
vapid.generate_keys()
|
||||||
privkey = vapid.private_pem()
|
privkey = vapid.private_pem()
|
||||||
assert vapid.public_key, "VAPID public key does not exist"
|
if not vapid.public_key:
|
||||||
|
raise ValueError("VAPID public key does not exist")
|
||||||
pubkey = b64urlencode(
|
pubkey = b64urlencode(
|
||||||
vapid.public_key.public_bytes(
|
vapid.public_key.public_bytes(
|
||||||
serialization.Encoding.X962,
|
serialization.Encoding.X962,
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ async def create_user_account_no_ckeck(
|
|||||||
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
logger.error(f"Error enabeling default extension {ext_id}: {e}")
|
||||||
|
|
||||||
user = await get_user_from_account(account)
|
user = await get_user_from_account(account)
|
||||||
assert user, "Cannot find user for account."
|
if not user:
|
||||||
|
raise ValueError("Cannot find user for account.")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -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.payments import get_payments_status_count
|
||||||
from lnbits.core.crud.users import get_accounts
|
from lnbits.core.crud.users import get_accounts
|
||||||
from lnbits.core.crud.wallets import get_wallets_count
|
from lnbits.core.crud.wallets import get_wallets_count
|
||||||
from lnbits.core.models import AuditEntry
|
from lnbits.core.models.audit import AuditEntry
|
||||||
from lnbits.core.models.extensions import InstallableExtension
|
from lnbits.core.models.extensions import InstallableExtension
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services.funding_source import (
|
from lnbits.core.services.funding_source import (
|
||||||
@@ -22,7 +22,7 @@ from lnbits.core.services.funding_source import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.notifications import (
|
from lnbits.core.services.notifications import (
|
||||||
enqueue_notification,
|
enqueue_admin_notification,
|
||||||
process_next_notification,
|
process_next_notification,
|
||||||
send_payment_notification,
|
send_payment_notification,
|
||||||
)
|
)
|
||||||
@@ -31,10 +31,10 @@ from lnbits.settings import settings
|
|||||||
from lnbits.tasks import create_unique_task
|
from lnbits.tasks import create_unique_task
|
||||||
from lnbits.utils.exchange_rates import btc_rates
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue[AuditEntry] = asyncio.Queue()
|
||||||
|
|
||||||
|
|
||||||
async def run_by_the_minute_tasks():
|
async def run_by_the_minute_tasks() -> None:
|
||||||
minute_counter = 0
|
minute_counter = 0
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||||
@@ -69,7 +69,7 @@ async def run_by_the_minute_tasks():
|
|||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
async def _notify_server_status():
|
async def _notify_server_status() -> None:
|
||||||
accounts = await get_accounts(filters=Filters(limit=0))
|
accounts = await get_accounts(filters=Filters(limit=0))
|
||||||
wallets_count = await get_wallets_count()
|
wallets_count = await get_wallets_count()
|
||||||
payments = await get_payments_status_count()
|
payments = await get_payments_status_count()
|
||||||
@@ -87,10 +87,10 @@ async def _notify_server_status():
|
|||||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||||
"node_balance_sats": status.node_balance_sats,
|
"node_balance_sats": status.node_balance_sats,
|
||||||
}
|
}
|
||||||
enqueue_notification(NotificationType.server_status, values)
|
enqueue_admin_notification(NotificationType.server_status, values)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
|
||||||
"""
|
"""
|
||||||
This worker dispatches events to all extensions and dispatches webhooks.
|
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):
|
|||||||
await send_payment_notification(wallet, payment)
|
await send_payment_notification(wallet, payment)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_audit_data():
|
async def wait_for_audit_data() -> None:
|
||||||
"""
|
"""
|
||||||
Waits for audit entries to be pushed to the queue.
|
Waits for audit entries to be pushed to the queue.
|
||||||
Then it inserts the entries into the DB.
|
Then it inserts the entries into the DB.
|
||||||
"""
|
"""
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
data: AuditEntry = await audit_queue.get()
|
data = await audit_queue.get()
|
||||||
try:
|
try:
|
||||||
await create_audit_entry(data)
|
await create_audit_entry(data)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -117,17 +117,17 @@ async def wait_for_audit_data():
|
|||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
async def wait_notification_messages():
|
async def wait_notification_messages() -> None:
|
||||||
|
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
try:
|
try:
|
||||||
await process_next_notification()
|
await process_next_notification()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.log("error", ex)
|
logger.warning("Payment notification error", ex)
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
async def purge_audit_data():
|
async def purge_audit_data() -> None:
|
||||||
"""
|
"""
|
||||||
Remove audit entries which have passed their retention period.
|
Remove audit entries which have passed their retention period.
|
||||||
"""
|
"""
|
||||||
@@ -141,7 +141,7 @@ async def purge_audit_data():
|
|||||||
await asyncio.sleep(60 * 60)
|
await asyncio.sleep(60 * 60)
|
||||||
|
|
||||||
|
|
||||||
async def collect_exchange_rates_data():
|
async def collect_exchange_rates_data() -> None:
|
||||||
"""
|
"""
|
||||||
Collect exchange rates data. Used for monitoring only.
|
Collect exchange rates data. Used for monitoring only.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -145,7 +145,9 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:label="$t('ad_slots_label')"
|
:label="$t('ad_slots_label')"
|
||||||
:hint="$t('ad_slots_hint')"
|
:hint="$t('ad_slots_hint')"
|
||||||
>
|
><q-tooltip>
|
||||||
|
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
|
||||||
|
</q-tooltip>
|
||||||
</q-input>
|
</q-input>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formData.lnbits_ad_space_enabled"
|
v-model="formData.lnbits_ad_space_enabled"
|
||||||
@@ -154,9 +156,9 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p><span v-text="$t('ui_default_theme')"></span></p>
|
<div class="row q-col-gutter-md q-my-md">
|
||||||
<div class="row q-col-gutter-md">
|
<div class="col-12" v-text="$t('ui_default_theme')"></div>
|
||||||
<div class="col">
|
<div class="col-12 col-sm-6 col-lg-3">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="formData.lnbits_default_border"
|
v-model="formData.lnbits_default_border"
|
||||||
:options="globalBorderOptions"
|
:options="globalBorderOptions"
|
||||||
@@ -165,7 +167,7 @@
|
|||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-sm-6 col-lg-3">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="formData.lnbits_default_theme"
|
v-model="formData.lnbits_default_theme"
|
||||||
:options="lnbits_theme_options"
|
:options="lnbits_theme_options"
|
||||||
@@ -174,7 +176,7 @@
|
|||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-sm-6 col-lg-3">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="formData.lnbits_default_reaction"
|
v-model="formData.lnbits_default_reaction"
|
||||||
:options="reactionOptions"
|
:options="reactionOptions"
|
||||||
@@ -183,7 +185,7 @@
|
|||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-sm-6 col-lg-3">
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_default_bgimage"
|
v-model="formData.lnbits_default_bgimage"
|
||||||
@@ -193,7 +195,7 @@
|
|||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-sm-6 col-lg-3">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
type="bool"
|
type="bool"
|
||||||
v-model="formData.lnbits_default_gradient"
|
v-model="formData.lnbits_default_gradient"
|
||||||
|
|||||||
@@ -7,38 +7,71 @@
|
|||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-card>
|
<q-card>
|
||||||
<div>
|
<div class="q-pa-sm">
|
||||||
<div class="q-gutter-y-md">
|
<div class="row items-center justify-between q-gutter-xs">
|
||||||
<q-tabs v-model="tab" align="left">
|
<div class="col">
|
||||||
<q-tab
|
<q-btn @click="updateAccount" unelevated color="primary">
|
||||||
name="user"
|
<span v-text="$t('update_account')"></span>
|
||||||
:label="$t('account_settings')"
|
</q-btn>
|
||||||
@update="val => tab = val.name"
|
</div>
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="theme"
|
|
||||||
:label="$t('look_and_feel')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
name="api_acls"
|
|
||||||
:label="$t('access_control_list')"
|
|
||||||
@update="val => tab = val.name"
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div v-if="user" class="col-md-12 col-lg-6 q-gutter-y-md">
|
<div class="col-12">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-splitter>
|
||||||
<div class="row">
|
<template v-slot:before>
|
||||||
<div class="col">
|
<q-tabs v-model="tab" vertical active-color="primary">
|
||||||
<q-tab-panels v-model="tab">
|
<q-tab
|
||||||
|
name="user"
|
||||||
|
icon="person"
|
||||||
|
:label="$q.screen.gt.sm ? $t('account_settings') : ''"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('account_settings')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
<q-tab
|
||||||
|
name="notifications"
|
||||||
|
icon="notifications"
|
||||||
|
:label="$q.screen.gt.sm ? $t('notifications') : ''"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('notifications')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="theme"
|
||||||
|
icon="palette"
|
||||||
|
:label="$q.screen.gt.sm ? $t('look_and_feel') : ''"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('look_and_feel')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-tab>
|
||||||
|
<q-tab
|
||||||
|
name="api_acls"
|
||||||
|
icon="lock"
|
||||||
|
:label="$q.screen.gt.sm ? $t('access_control_list') : ''"
|
||||||
|
@update="val => tab = val.name"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="!$q.screen.gt.sm"
|
||||||
|
><span v-text="$t('access_control_list')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
</template>
|
||||||
|
<template v-slot:after>
|
||||||
|
<q-scroll-area style="height: 80vh">
|
||||||
|
<q-tab-panels v-if="user" v-model="tab">
|
||||||
<q-tab-panel name="user">
|
<q-tab-panel name="user">
|
||||||
<div v-if="credentialsData.show">
|
<div v-if="credentialsData.show">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -293,9 +326,6 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn @click="updateAccount" unelevated color="primary">
|
|
||||||
<span v-text="$t('update_account')"></span>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('change_password')"
|
:label="$t('change_password')"
|
||||||
@@ -559,12 +589,101 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
</q-tab-panel>
|
||||||
<div class="row q-mb-md q-mt-md">
|
<q-tab-panel name="notifications">
|
||||||
<q-btn @click="updateAccount" unelevated color="primary">
|
<q-card-section>
|
||||||
<span v-text="$t('update_account')"></span>
|
<div class="row q-mb-md">
|
||||||
</q-btn>
|
<div class="col-4">
|
||||||
</div>
|
<span
|
||||||
|
v-text="$t('notifications_nostr_identifier')"
|
||||||
|
></span>
|
||||||
|
{%if not nostr_configured%}
|
||||||
|
<br />
|
||||||
|
<q-badge v-text="$t('not_connected')"></q-badge>
|
||||||
|
{%endif%}
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="user.extra.notifications.nostr_identifier"
|
||||||
|
:hint="$t('notifications_nostr_identifier_desc')"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('notifications_chat_id')"></span>
|
||||||
|
{%if not telegram_configured%}
|
||||||
|
<br />
|
||||||
|
<q-badge v-text="$t('not_connected')"></q-badge>
|
||||||
|
{%endif%}
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="user.extra.notifications.telegram_chat_id"
|
||||||
|
:hint="$t('notifications_chat_id_desc')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-separator class="q-mb-md"></q-separator>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('notification_outgoing_payment')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="user.extra.notifications.outgoing_payments_sats"
|
||||||
|
:hint="$t('notification_outgoing_payment_desc')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('notification_incoming_payment')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="user.extra.notifications.incoming_payments_sats"
|
||||||
|
:hint="$t('notification_incoming_payment_desc')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('exclude_wallets')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
multiple
|
||||||
|
v-model="user.extra.notifications.excluded_wallets"
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
:label="$t('exclude_wallets')"
|
||||||
|
:hint="$t('notifications_excluded_wallets_desc')"
|
||||||
|
class="q-mt-sm"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="api_acls">
|
<q-tab-panel name="api_acls">
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
@@ -791,16 +910,9 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</div>
|
</q-scroll-area>
|
||||||
</div>
|
</template>
|
||||||
</q-card-section>
|
</q-splitter>
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div v-else class="col-12 col-md-6 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -661,14 +661,48 @@
|
|||||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||||
></q-input>
|
></q-input>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<q-input
|
||||||
|
v-if="has_holdinvoice"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="receive.data.payment_hash"
|
||||||
|
:label="$t('hold_invoice_payment_hash')"
|
||||||
|
></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="2"
|
rows="2"
|
||||||
v-model.trim="receive.data.memo"
|
v-model="receive.data.memo"
|
||||||
:label="$t('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>
|
></q-input>
|
||||||
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
|
<div v-if="g.user.fiat_providers?.length" class="q-mt-md">
|
||||||
<q-list bordered dense class="rounded-borders">
|
<q-list bordered dense class="rounded-borders">
|
||||||
@@ -848,6 +882,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<h6 class="text-center" v-text="parse.invoice.description"></h6>
|
<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-list separator bordered dense class="q-mb-md">
|
||||||
<q-expansion-item expand-separator icon="info" label="Details">
|
<q-expansion-item expand-separator icon="info" label="Details">
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
@@ -1041,7 +1090,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col q-mb-lg">
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
@@ -1075,9 +1124,39 @@
|
|||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="parse.data.comment"
|
v-model="parse.data.comment"
|
||||||
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
|
:type="parse.lnurlpay.commentAllowed > 512 ? 'textarea' : 'text'"
|
||||||
label="Comment (optional)"
|
label="Comment (optional)"
|
||||||
:maxlength="parse.lnurlpay.commentAllowed"
|
: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>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from lnbits.core.models import User
|
|||||||
from lnbits.core.models.misc import Image, SimpleStatus
|
from lnbits.core.models.misc import Image, SimpleStatus
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_notification,
|
enqueue_admin_notification,
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
@@ -65,7 +65,9 @@ async def api_monitor():
|
|||||||
)
|
)
|
||||||
async def api_test_email():
|
async def api_test_email():
|
||||||
return await send_email_notification(
|
return await send_email_notification(
|
||||||
"This is a LNbits test email.", "LNbits Test Email"
|
settings.lnbits_email_notifications_to_emails,
|
||||||
|
"This is a LNbits test email.",
|
||||||
|
"LNbits Test Email",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -82,10 +84,13 @@ async def api_get_settings(
|
|||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
||||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
enqueue_admin_notification(
|
||||||
|
NotificationType.settings_update, {"username": user.username}
|
||||||
|
)
|
||||||
await update_admin_settings(data)
|
await update_admin_settings(data)
|
||||||
admin_settings = await get_admin_settings(user.super_user)
|
admin_settings = await get_admin_settings(user.super_user)
|
||||||
assert admin_settings, "Updated admin settings not found."
|
if not admin_settings:
|
||||||
|
raise ValueError("Updated admin settings not found.")
|
||||||
update_cached_settings(admin_settings.dict())
|
update_cached_settings(admin_settings.dict())
|
||||||
core_app_extra.register_new_ratelimiter()
|
core_app_extra.register_new_ratelimiter()
|
||||||
return {"status": "Success"}
|
return {"status": "Success"}
|
||||||
@@ -112,7 +117,9 @@ async def api_reset_settings(field_name: str):
|
|||||||
|
|
||||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
enqueue_admin_notification(
|
||||||
|
NotificationType.settings_update, {"username": user.username}
|
||||||
|
)
|
||||||
await reset_core_settings()
|
await reset_core_settings()
|
||||||
server_restart.set()
|
server_restart.set()
|
||||||
|
|
||||||
|
|||||||
+3
-156
@@ -1,36 +1,20 @@
|
|||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from fastapi import (
|
from fastapi import APIRouter, Depends
|
||||||
APIRouter,
|
|
||||||
Depends,
|
|
||||||
)
|
|
||||||
from fastapi.exceptions import HTTPException
|
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
ConversionData,
|
ConversionData,
|
||||||
CreateLnurlAuth,
|
|
||||||
CreateWallet,
|
CreateWallet,
|
||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import check_user_exists
|
||||||
WalletTypeInfo,
|
|
||||||
check_user_exists,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
@@ -41,7 +25,7 @@ from lnbits.utils.exchange_rates import (
|
|||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import StatusResponse
|
from lnbits.wallets.base import StatusResponse
|
||||||
|
|
||||||
from ..services import create_user_account, perform_lnurlauth
|
from ..services import create_user_account
|
||||||
|
|
||||||
api_router = APIRouter(tags=["Core"])
|
api_router = APIRouter(tags=["Core"])
|
||||||
|
|
||||||
@@ -92,143 +76,6 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
|||||||
return user.wallets[0]
|
return user.wallets[0]
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/lnurlscan/{code}")
|
|
||||||
async def api_lnurlscan(
|
|
||||||
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
url = str(lnurl_decode(code))
|
|
||||||
domain = urlparse(url).netloc
|
|
||||||
except Exception as exc:
|
|
||||||
# parse internet identifier (user@domain.com)
|
|
||||||
name_domain = code.split("@")
|
|
||||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
|
||||||
name, domain = name_domain
|
|
||||||
url = (
|
|
||||||
("http://" if domain.endswith(".onion") else "https://")
|
|
||||||
+ domain
|
|
||||||
+ "/.well-known/lnurlp/"
|
|
||||||
+ name
|
|
||||||
)
|
|
||||||
# will proceed with these values
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# params is what will be returned to the client
|
|
||||||
params: dict = {"domain": domain}
|
|
||||||
|
|
||||||
if "tag=login" in url:
|
|
||||||
params.update(kind="auth")
|
|
||||||
params.update(callback=url) # with k1 already in it
|
|
||||||
|
|
||||||
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
|
|
||||||
assert lnurlauth_key.verifying_key
|
|
||||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
|
||||||
else:
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
|
||||||
check_callback_url(url)
|
|
||||||
try:
|
|
||||||
r = await client.get(url, timeout=5)
|
|
||||||
r.raise_for_status()
|
|
||||||
except httpx.HTTPStatusError as exc:
|
|
||||||
if exc.response.status_code == 404:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
|
|
||||||
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": "failed to get parameters",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
try:
|
|
||||||
data = json.loads(r.text)
|
|
||||||
except json.decoder.JSONDecodeError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": f"got invalid response '{r.text[:200]}'",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
try:
|
|
||||||
tag: str = data.get("tag")
|
|
||||||
params.update(**data)
|
|
||||||
if tag == "channelRequest":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"kind": "channel",
|
|
||||||
"message": "unsupported",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif tag == "withdrawRequest":
|
|
||||||
params.update(kind="withdraw")
|
|
||||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
|
||||||
|
|
||||||
# callback with k1 already in it
|
|
||||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
|
||||||
qs: dict = parse_qs(parsed_callback.query)
|
|
||||||
qs["k1"] = data["k1"]
|
|
||||||
|
|
||||||
# balanceCheck/balanceNotify
|
|
||||||
if "balanceCheck" in data:
|
|
||||||
params.update(balanceCheck=data["balanceCheck"])
|
|
||||||
|
|
||||||
# format callback url and send to client
|
|
||||||
parsed_callback = parsed_callback._replace(
|
|
||||||
query=urlencode(qs, doseq=True)
|
|
||||||
)
|
|
||||||
params.update(callback=urlunparse(parsed_callback))
|
|
||||||
elif tag == "payRequest":
|
|
||||||
params.update(kind="pay")
|
|
||||||
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
|
||||||
|
|
||||||
params.update(
|
|
||||||
description_hash=hashlib.sha256(
|
|
||||||
data["metadata"].encode()
|
|
||||||
).hexdigest()
|
|
||||||
)
|
|
||||||
metadata = json.loads(data["metadata"])
|
|
||||||
for [k, v] in metadata:
|
|
||||||
if k == "text/plain":
|
|
||||||
params.update(description=v)
|
|
||||||
if k in ("image/jpeg;base64", "image/png;base64"):
|
|
||||||
data_uri = f"data:{k},{v}"
|
|
||||||
params.update(image=data_uri)
|
|
||||||
if k in ("text/email", "text/identifier"):
|
|
||||||
params.update(targetUser=v)
|
|
||||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
|
||||||
|
|
||||||
except KeyError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": f"lnurl JSON response invalid: {exc}",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/api/v1/lnurlauth")
|
|
||||||
async def api_perform_lnurlauth(
|
|
||||||
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
err = await perform_lnurlauth(data.callback, wallet=wallet)
|
|
||||||
if err:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get(
|
@api_router.get(
|
||||||
"/api/v1/rate/history",
|
"/api/v1/rate/history",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(check_user_exists)],
|
||||||
|
|||||||
@@ -191,12 +191,14 @@ async def api_create_user_api_token(
|
|||||||
data: ApiTokenRequest,
|
data: ApiTokenRequest,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> ApiTokenResponse:
|
) -> ApiTokenResponse:
|
||||||
assert data.expiration_time_minutes > 0, "Expiration time must be in the future."
|
if not data.expiration_time_minutes > 0:
|
||||||
|
raise ValueError("Expiration time must be in the future.")
|
||||||
account = await get_account(user.id)
|
account = await get_account(user.id)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
assert account.username, "Username must be configured."
|
if not account.username:
|
||||||
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(user.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
@@ -223,7 +225,8 @@ async def api_delete_user_api_token(
|
|||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
assert account.username, "Username must be configured."
|
if not account.username:
|
||||||
|
raise ValueError("Username must be configured.")
|
||||||
|
|
||||||
acls = await get_user_access_control_lists(user.id)
|
acls = await get_user_access_control_lists(user.id)
|
||||||
acl = acls.get_acl_by_id(data.acl_id)
|
acl = acls.get_acl_by_id(data.acl_id)
|
||||||
@@ -318,7 +321,7 @@ async def update_pubkey(
|
|||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
raise ValueError("Invalid user ID.")
|
||||||
|
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
if (
|
if (
|
||||||
@@ -326,7 +329,7 @@ async def update_pubkey(
|
|||||||
and data.pubkey != user.pubkey
|
and data.pubkey != user.pubkey
|
||||||
and await get_account_by_pubkey(data.pubkey)
|
and await get_account_by_pubkey(data.pubkey)
|
||||||
):
|
):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Public key already in use.")
|
raise ValueError("Public key already in use.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
account = await get_account(user.id)
|
||||||
if not account:
|
if not account:
|
||||||
@@ -344,7 +347,8 @@ async def update_password(
|
|||||||
payload: AccessTokenPayload = Depends(access_token_payload),
|
payload: AccessTokenPayload = Depends(access_token_payload),
|
||||||
) -> Optional[User]:
|
) -> Optional[User]:
|
||||||
_validate_auth_timeout(payload.auth_time)
|
_validate_auth_timeout(payload.auth_time)
|
||||||
assert data.user_id == user.id, "Invalid user ID."
|
if data.user_id != user.id:
|
||||||
|
raise ValueError("Invalid user ID.")
|
||||||
if (
|
if (
|
||||||
data.username
|
data.username
|
||||||
and user.username != data.username
|
and user.username != data.username
|
||||||
@@ -353,12 +357,15 @@ async def update_password(
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
account = await get_account(user.id)
|
account = await get_account(user.id)
|
||||||
assert account, "Account not found."
|
if not account:
|
||||||
|
raise ValueError("Account not found.")
|
||||||
|
|
||||||
# old accounts do not have a password
|
# old accounts do not have a password
|
||||||
if account.password_hash:
|
if account.password_hash:
|
||||||
assert data.password_old, "Missing old password."
|
if not data.password_old:
|
||||||
assert account.verify_password(data.password_old), "Invalid old password."
|
raise ValueError("Missing old password.")
|
||||||
|
if not account.verify_password(data.password_old):
|
||||||
|
raise ValueError("Invalid old password.")
|
||||||
|
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
@@ -376,8 +383,10 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
|
HTTPStatus.FORBIDDEN, "Auth by 'Username and Password' not allowed."
|
||||||
)
|
)
|
||||||
|
|
||||||
assert data.password == data.password_repeat, "Passwords do not match."
|
if data.password != data.password_repeat:
|
||||||
assert data.reset_key[:10].startswith("reset_key_"), "This is not a reset key."
|
raise ValueError("Passwords do not match.")
|
||||||
|
if not data.reset_key[:10].startswith("reset_key_"):
|
||||||
|
raise ValueError("This is not a reset key.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reset_key = base64.b64decode(data.reset_key[10:]).decode()
|
reset_key = base64.b64decode(data.reset_key[10:]).decode()
|
||||||
@@ -385,12 +394,16 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValueError("Invalid reset key.") from exc
|
raise ValueError("Invalid reset key.") from exc
|
||||||
|
|
||||||
assert reset_data_json, "Cannot process reset key."
|
if not reset_data_json:
|
||||||
|
raise ValueError("Cannot process reset key.")
|
||||||
|
|
||||||
action, user_id, request_time = json.loads(reset_data_json)
|
action, user_id, request_time = json.loads(reset_data_json)
|
||||||
assert action, "Missing action."
|
if not action:
|
||||||
assert user_id, "Missing user ID."
|
raise ValueError("Missing action.")
|
||||||
assert request_time, "Missing reset time."
|
if not user_id:
|
||||||
|
raise ValueError("Missing user ID.")
|
||||||
|
if not request_time:
|
||||||
|
raise ValueError("Missing reset time.")
|
||||||
|
|
||||||
_validate_auth_timeout(request_time)
|
_validate_auth_timeout(request_time)
|
||||||
|
|
||||||
@@ -557,8 +570,8 @@ def _find_auth_provider_class(provider: str) -> Callable:
|
|||||||
provider_class = getattr(provider_module, f"{provider.title()}SSO")
|
provider_class = getattr(provider_module, f"{provider.title()}SSO")
|
||||||
if provider_class:
|
if provider_class:
|
||||||
return provider_class
|
return provider_class
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logger.debug(exc)
|
||||||
|
|
||||||
raise ValueError(f"No SSO provider found for '{provider}'.")
|
raise ValueError(f"No SSO provider found for '{provider}'.")
|
||||||
|
|
||||||
@@ -576,28 +589,40 @@ def _nostr_nip98_event(request: Request) -> dict:
|
|||||||
event = json.loads(event_json)
|
event = json.loads(event_json)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
assert event, "Nostr login event cannot be parsed."
|
if not event:
|
||||||
|
raise ValueError("Nostr login event cannot be parsed.")
|
||||||
|
|
||||||
if not verify_event(event):
|
if not verify_event(event):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Nostr login event is not valid.")
|
||||||
assert event["kind"] == 27_235, "Invalid event kind."
|
if not event["kind"] == 27_235:
|
||||||
|
raise ValueError("Invalid event kind.")
|
||||||
|
|
||||||
auth_threshold = settings.auth_credetials_update_threshold
|
auth_threshold = settings.auth_credetials_update_threshold
|
||||||
assert (
|
if not (abs(time() - event["created_at"]) < auth_threshold):
|
||||||
abs(time() - event["created_at"]) < auth_threshold
|
raise ValueError(
|
||||||
), f"More than {auth_threshold} seconds have passed since the event was signed."
|
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)
|
method: Optional[str] = next((v for k, v in event["tags"] if k == "method"), None)
|
||||||
assert method, "Tag 'method' is missing."
|
if not method:
|
||||||
assert method.upper() == "POST", "Invalid value for tag 'method'."
|
raise ValueError("Tag 'method' is missing.")
|
||||||
|
if not method.upper() == "POST":
|
||||||
|
raise ValueError("Invalid value for tag 'method'.")
|
||||||
|
|
||||||
url = next((v for k, v in event["tags"] if k == "u"), None)
|
url = next((v for k, v in event["tags"] if k == "u"), None)
|
||||||
|
|
||||||
assert url, "Tag 'u' for URL is missing."
|
if not url:
|
||||||
|
raise ValueError("Tag 'u' for URL is missing.")
|
||||||
accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls]
|
accepted_urls = [f"{u}/nostr" for u in settings.nostr_absolute_request_urls]
|
||||||
assert url in accepted_urls, f"Invalid value for tag 'u': '{url}'."
|
if url not in accepted_urls:
|
||||||
|
raise ValueError(f"Invalid value for tag 'u': '{url}'.")
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_auth_timeout(auth_time: Optional[int] = 0):
|
def _validate_auth_timeout(auth_time: Optional[int] = 0):
|
||||||
|
|||||||
@@ -170,64 +170,52 @@ async def api_enable_extension(
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
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."
|
|
||||||
|
|
||||||
user_ext = await get_user_extension(user.id, ext_id)
|
logger.info(f"Enabling extension: {ext_id}.")
|
||||||
if not user_ext:
|
ext = await get_installed_extension(ext_id)
|
||||||
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
if not ext:
|
||||||
await create_user_extension(user_ext)
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
|
if not ext.active:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||||
|
|
||||||
if user.admin or not ext.requires_payment:
|
user_ext = await get_user_extension(user.id, ext_id)
|
||||||
user_ext.active = True
|
if not user_ext:
|
||||||
await update_user_extension(user_ext)
|
user_ext = UserExtension(user=user.id, extension=ext_id, active=False)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
await create_user_extension(user_ext)
|
||||||
|
|
||||||
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
if user.admin or not ext.requires_payment:
|
||||||
raise HTTPException(
|
user_ext.active = True
|
||||||
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
await update_user_extension(user_ext)
|
||||||
)
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||||
|
|
||||||
if user_ext.is_paid:
|
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
||||||
user_ext.active = True
|
raise HTTPException(
|
||||||
await update_user_extension(user_ext)
|
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
||||||
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 not payment_status.paid:
|
if user_ext.is_paid:
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
|
||||||
)
|
|
||||||
|
|
||||||
user_ext.active = True
|
user_ext.active = True
|
||||||
user_ext.extra.paid_to_enable = True
|
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||||
|
|
||||||
except AssertionError as exc:
|
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
|
raise ValueError(f"Extension '{ext_id}' is missing payment wallet.")
|
||||||
except HTTPException as exc:
|
|
||||||
raise exc from exc
|
payment_status = await check_transaction_status(
|
||||||
except Exception as exc:
|
wallet_id=ext.meta.pay_to_enable.wallet,
|
||||||
logger.warning(exc)
|
payment_hash=user_ext.extra.payment_hash_to_enable,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_status.paid:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
HTTPStatus.PAYMENT_REQUIRED,
|
||||||
detail=(f"Failed to enable '{ext_id}' "),
|
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
||||||
) from exc
|
)
|
||||||
|
|
||||||
|
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.")
|
||||||
|
|
||||||
|
|
||||||
@extension_router.put("/{ext_id}/disable")
|
@extension_router.put("/{ext_id}/disable")
|
||||||
@@ -243,7 +231,7 @@ async def api_disable_extension(
|
|||||||
return SimpleStatus(
|
return SimpleStatus(
|
||||||
success=True, message=f"Extension '{ext_id}' already disabled."
|
success=True, message=f"Extension '{ext_id}' already disabled."
|
||||||
)
|
)
|
||||||
logger.info(f"Disabeling extension: {ext_id}.")
|
logger.info(f"Disabling extension: {ext_id}.")
|
||||||
user_ext.active = False
|
user_ext.active = False
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||||
@@ -255,7 +243,8 @@ async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
|||||||
logger.info(f"Activating extension: '{ext_id}'.")
|
logger.info(f"Activating extension: '{ext_id}'.")
|
||||||
|
|
||||||
ext = await get_valid_extension(ext_id)
|
ext = await get_valid_extension(ext_id)
|
||||||
assert ext, f"Extension '{ext_id}' doesn't exist."
|
if not ext:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' doesn't exist.")
|
||||||
|
|
||||||
await activate_extension(ext)
|
await activate_extension(ext)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' activated.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' activated.")
|
||||||
@@ -274,7 +263,8 @@ async def api_deactivate_extension(ext_id: str) -> SimpleStatus:
|
|||||||
logger.info(f"Deactivating extension: '{ext_id}'.")
|
logger.info(f"Deactivating extension: '{ext_id}'.")
|
||||||
|
|
||||||
ext = await get_valid_extension(ext_id)
|
ext = await get_valid_extension(ext_id)
|
||||||
assert ext, f"Extension '{ext_id}' doesn't exist."
|
if not ext:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' doesn't exist.")
|
||||||
|
|
||||||
await deactivate_extension(ext_id)
|
await deactivate_extension(ext_id)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' deactivated.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' deactivated.")
|
||||||
@@ -354,40 +344,35 @@ async def get_extension_releases(ext_id: str) -> list[ExtensionRelease]:
|
|||||||
async def get_pay_to_install_invoice(
|
async def get_pay_to_install_invoice(
|
||||||
ext_id: str, data: CreateExtension
|
ext_id: str, data: CreateExtension
|
||||||
) -> ReleasePaymentInfo:
|
) -> ReleasePaymentInfo:
|
||||||
try:
|
if ext_id != data.ext_id:
|
||||||
assert (
|
raise ValueError(
|
||||||
ext_id == data.ext_id
|
f"Wrong extension id. Expected {ext_id}, but got {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
|
|
||||||
)
|
)
|
||||||
assert release, "Release not found."
|
if not data.cost_sats:
|
||||||
assert release.pay_link, "Pay link not found for release."
|
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.")
|
||||||
|
|
||||||
payment_info = await release.fetch_release_payment_info(data.cost_sats)
|
payment_info = await release.fetch_release_payment_info(data.cost_sats)
|
||||||
|
|
||||||
assert payment_info and payment_info.payment_request, "Cannot request invoice."
|
if not (payment_info and payment_info.payment_request):
|
||||||
invoice = bolt11_decode(payment_info.payment_request)
|
raise ValueError("Cannot request invoice.")
|
||||||
|
invoice = bolt11_decode(payment_info.payment_request)
|
||||||
|
|
||||||
assert invoice.amount_msat is not None, "Invoic amount is missing."
|
if invoice.amount_msat is None:
|
||||||
invoice_amount = int(invoice.amount_msat / 1000)
|
raise ValueError("Invoic amount is missing.")
|
||||||
assert (
|
invoice_amount = int(invoice.amount_msat / 1000)
|
||||||
invoice_amount == data.cost_sats
|
if invoice_amount != data.cost_sats:
|
||||||
), f"Wrong invoice amount: {invoice_amount}."
|
raise ValueError(f"Wrong invoice amount: {invoice_amount}.")
|
||||||
assert (
|
if payment_info.payment_hash != invoice.payment_hash:
|
||||||
payment_info.payment_hash == invoice.payment_hash
|
raise ValueError("Wrong 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")
|
@extension_router.put("/{ext_id}/invoice/enable")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fastapi import Cookie, Depends, Query, Request
|
|||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from lnurl import decode as lnurl_decode
|
from lnurl import url_decode
|
||||||
from pydantic.types import UUID4
|
from pydantic.types import UUID4
|
||||||
|
|
||||||
from lnbits.core.helpers import to_valid_user_id
|
from lnbits.core.helpers import to_valid_user_id
|
||||||
@@ -206,11 +206,15 @@ async def account(
|
|||||||
request: Request,
|
request: Request,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
nostr_configured = settings.is_nostr_notifications_configured()
|
||||||
|
telegram_configured = settings.is_telegram_notifications_configured()
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"core/account.html",
|
"core/account.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
|
"nostr_configured": nostr_configured,
|
||||||
|
"telegram_configured": telegram_configured,
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -341,7 +345,6 @@ async def node(request: Request, user: User = Depends(check_admin)):
|
|||||||
"node/index.html",
|
"node/index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"settings": settings.dict(),
|
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"wallets": user.wallets[0].json(),
|
"wallets": user.wallets[0].json(),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
@@ -361,7 +364,6 @@ async def node_public(request: Request):
|
|||||||
request,
|
request,
|
||||||
"node/public.html",
|
"node/public.html",
|
||||||
{
|
{
|
||||||
"settings": settings.dict(),
|
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -380,7 +382,6 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
"admin/index.html",
|
"admin/index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"settings": settings.dict(),
|
|
||||||
"balance": balance,
|
"balance": balance,
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
@@ -398,7 +399,6 @@ async def users_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
"settings": settings.dict(),
|
|
||||||
"currencies": list(currencies.keys()),
|
"currencies": list(currencies.keys()),
|
||||||
"ajax": _is_ajax_request(request),
|
"ajax": _is_ajax_request(request),
|
||||||
},
|
},
|
||||||
@@ -456,7 +456,7 @@ async def lnurlwallet(request: Request, lightning: str = ""):
|
|||||||
if not settings.lnbits_allow_new_accounts:
|
if not settings.lnbits_allow_new_accounts:
|
||||||
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
return {"status": "ERROR", "reason": "New accounts are not allowed."}
|
||||||
|
|
||||||
lnurl = lnurl_decode(lightning)
|
lnurl = url_decode(lightning)
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
check_callback_url(lnurl)
|
check_callback_url(lnurl)
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
)
|
||||||
|
from lnurl import (
|
||||||
|
LnurlResponseException,
|
||||||
|
LnurlSuccessResponse,
|
||||||
|
)
|
||||||
|
from lnurl import execute_login as lnurlauth
|
||||||
|
from lnurl import execute_withdraw as lnurl_withdraw
|
||||||
|
from lnurl import handle as lnurl_handle
|
||||||
|
from lnurl.models import (
|
||||||
|
LnurlAuthResponse,
|
||||||
|
LnurlErrorResponse,
|
||||||
|
LnurlPayResponse,
|
||||||
|
LnurlResponseModel,
|
||||||
|
LnurlWithdrawResponse,
|
||||||
|
)
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.models import CreateLnurlWithdraw, Payment
|
||||||
|
from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan
|
||||||
|
from lnbits.decorators import (
|
||||||
|
WalletTypeInfo,
|
||||||
|
require_admin_key,
|
||||||
|
require_invoice_key,
|
||||||
|
)
|
||||||
|
from lnbits.helpers import check_callback_url
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from ..services import fetch_lnurl_pay_request, pay_invoice
|
||||||
|
|
||||||
|
lnurl_router = APIRouter(tags=["LNURL"])
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.get(
|
||||||
|
"/api/v1/lnurlscan/{code}",
|
||||||
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
deprecated=True,
|
||||||
|
response_model=LnurlPayResponse
|
||||||
|
| LnurlWithdrawResponse
|
||||||
|
| LnurlAuthResponse
|
||||||
|
| LnurlErrorResponse,
|
||||||
|
)
|
||||||
|
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||||
|
try:
|
||||||
|
res = await lnurl_handle(code, user_agent=settings.user_agent, timeout=5)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
|
||||||
|
check_callback_url(res.callback)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post(
|
||||||
|
"/api/v1/lnurlscan",
|
||||||
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
response_model=LnurlPayResponse
|
||||||
|
| LnurlWithdrawResponse
|
||||||
|
| LnurlAuthResponse
|
||||||
|
| LnurlErrorResponse,
|
||||||
|
)
|
||||||
|
async def api_lnurlscan_post(scan: LnurlScan) -> LnurlResponseModel:
|
||||||
|
try:
|
||||||
|
res = await lnurl_handle(scan.lnurl, user_agent=settings.user_agent, timeout=5)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post("/api/v1/lnurlauth")
|
||||||
|
async def api_perform_lnurlauth(
|
||||||
|
data: LnurlAuthResponse, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> LnurlResponseModel:
|
||||||
|
check_callback_url(data.callback)
|
||||||
|
try:
|
||||||
|
res = await lnurlauth(
|
||||||
|
res=data,
|
||||||
|
seed=key_type.wallet.adminkey,
|
||||||
|
user_agent=settings.user_agent,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post("/api/v1/payments/lnurl")
|
||||||
|
async def api_payments_pay_lnurl(
|
||||||
|
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> Payment:
|
||||||
|
try:
|
||||||
|
res = await fetch_lnurl_pay_request(data=data)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
msg = f"Failed to connect to {data.res.callback}."
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) from exc
|
||||||
|
|
||||||
|
extra: dict[str, Any] = {}
|
||||||
|
if res.success_action:
|
||||||
|
extra["success_action"] = res.success_action.json()
|
||||||
|
if data.comment:
|
||||||
|
extra["comment"] = data.comment
|
||||||
|
if data.unit and data.unit != "sat":
|
||||||
|
extra["fiat_currency"] = data.unit
|
||||||
|
extra["fiat_amount"] = data.amount / 1000
|
||||||
|
|
||||||
|
payment = await pay_invoice(
|
||||||
|
wallet_id=wallet.wallet.id,
|
||||||
|
payment_request=str(res.pr),
|
||||||
|
description=data.res.metadata.text,
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post(
|
||||||
|
"/api/v1/payments/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK
|
||||||
|
)
|
||||||
|
async def api_payment_pay_with_nfc(
|
||||||
|
payment_request: str,
|
||||||
|
lnurl_data: CreateLnurlWithdraw,
|
||||||
|
) -> LnurlErrorResponse | LnurlSuccessResponse:
|
||||||
|
if not lnurl_data.lnurl_w.lud17:
|
||||||
|
return LnurlErrorResponse(reason="LNURL-withdraw lud17 not provided.")
|
||||||
|
try:
|
||||||
|
url = lnurl_data.lnurl_w.lud17
|
||||||
|
res = await lnurl_handle(url, user_agent=settings.user_agent, timeout=10)
|
||||||
|
except (LnurlResponseException, Exception) as exc:
|
||||||
|
return LnurlErrorResponse(reason=str(exc))
|
||||||
|
|
||||||
|
if not isinstance(res, LnurlWithdrawResponse):
|
||||||
|
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
|
||||||
|
try:
|
||||||
|
check_callback_url(res.callback)
|
||||||
|
except ValueError as exc:
|
||||||
|
return LnurlErrorResponse(reason=f"Invalid callback URL: {exc!s}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
res2 = await lnurl_withdraw(
|
||||||
|
res, payment_request, user_agent=settings.user_agent, timeout=10
|
||||||
|
)
|
||||||
|
except (LnurlResponseException, Exception) as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return LnurlErrorResponse(reason=str(exc))
|
||||||
|
if not isinstance(res2, (LnurlSuccessResponse, LnurlErrorResponse)):
|
||||||
|
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
|
||||||
|
|
||||||
|
return res2
|
||||||
@@ -7,8 +7,9 @@ from pydantic import BaseModel
|
|||||||
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||||
from lnbits.nodes import get_node_class
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.wallets import get_funding_source
|
||||||
|
from lnbits.wallets.base import Feature
|
||||||
|
|
||||||
from ...db import Filters, Page
|
from ...db import Filters, Page
|
||||||
from ...nodes.base import (
|
from ...nodes.base import (
|
||||||
@@ -26,9 +27,13 @@ from ...nodes.base import (
|
|||||||
from ...utils.cache import cache
|
from ...utils.cache import cache
|
||||||
|
|
||||||
|
|
||||||
def require_node():
|
def require_node() -> Node:
|
||||||
node_class = get_node_class()
|
funding_source = get_funding_source()
|
||||||
if not node_class:
|
if (
|
||||||
|
not funding_source.features
|
||||||
|
or Feature.nodemanager not in funding_source.features
|
||||||
|
or not funding_source.__node_cls__
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
||||||
detail="Active backend does not implement Node API",
|
detail="Active backend does not implement Node API",
|
||||||
@@ -38,7 +43,7 @@ def require_node():
|
|||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
detail="Not enabled",
|
detail="Not enabled",
|
||||||
)
|
)
|
||||||
return node_class
|
return funding_source.__node_cls__(funding_source)
|
||||||
|
|
||||||
|
|
||||||
def check_public():
|
def check_public():
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import json
|
from hashlib import sha256
|
||||||
import ssl
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from math import ceil
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
@@ -14,7 +10,7 @@ from fastapi import (
|
|||||||
Query,
|
Query,
|
||||||
)
|
)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from loguru import logger
|
from lnurl import url_decode
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud.payments import (
|
from lnbits.core.crud.payments import (
|
||||||
@@ -22,11 +18,10 @@ from lnbits.core.crud.payments import (
|
|||||||
get_wallets_stats,
|
get_wallets_stats,
|
||||||
)
|
)
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
|
CancelInvoice,
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreateLnurl,
|
|
||||||
DecodePayment,
|
DecodePayment,
|
||||||
KeyType,
|
KeyType,
|
||||||
PayLnurlWData,
|
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
PaymentCountField,
|
||||||
PaymentCountStat,
|
PaymentCountStat,
|
||||||
@@ -34,6 +29,7 @@ from lnbits.core.models import (
|
|||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
PaymentWalletStats,
|
PaymentWalletStats,
|
||||||
|
SettleInvoice,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
@@ -45,13 +41,10 @@ from lnbits.decorators import (
|
|||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
check_callback_url,
|
|
||||||
filter_dict_keys,
|
filter_dict_keys,
|
||||||
generate_filter_params_openapi,
|
generate_filter_params_openapi,
|
||||||
)
|
)
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
from lnbits.wallets.base import InvoiceResponse
|
||||||
from lnbits.settings import settings
|
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
DateTrunc,
|
DateTrunc,
|
||||||
@@ -62,11 +55,12 @@ from ..crud import (
|
|||||||
get_wallet_for_key,
|
get_wallet_for_key,
|
||||||
)
|
)
|
||||||
from ..services import (
|
from ..services import (
|
||||||
create_fiat_invoice,
|
cancel_hold_invoice,
|
||||||
create_wallet_invoice,
|
create_payment_request,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
get_payments_daily_stats,
|
get_payments_daily_stats,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
|
settle_hold_invoice,
|
||||||
update_pending_payment,
|
update_pending_payment,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
)
|
)
|
||||||
@@ -121,7 +115,6 @@ async def api_payments_counting_stats(
|
|||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
|
||||||
if user.admin:
|
if user.admin:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
@@ -142,7 +135,6 @@ async def api_payments_wallets_stats(
|
|||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
|
||||||
if user.admin:
|
if user.admin:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
@@ -163,7 +155,6 @@ async def api_payments_daily_stats(
|
|||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
|
|
||||||
if user.admin:
|
if user.admin:
|
||||||
# admin user can see payments from all wallets
|
# admin user can see payments from all wallets
|
||||||
for_user_id = None
|
for_user_id = None
|
||||||
@@ -264,10 +255,7 @@ async def api_payments_create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If the payment is not outgoing, we can create a new invoice.
|
# If the payment is not outgoing, we can create a new invoice.
|
||||||
if invoice_data.fiat_provider:
|
return await create_payment_request(wallet_id, invoice_data)
|
||||||
return await create_fiat_invoice(wallet_id, invoice_data)
|
|
||||||
|
|
||||||
return await create_wallet_invoice(wallet_id, invoice_data)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get("/fee-reserve")
|
@payment_router.get("/fee-reserve")
|
||||||
@@ -285,79 +273,6 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/lnurl")
|
|
||||||
async def api_payments_pay_lnurl(
|
|
||||||
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> Payment:
|
|
||||||
domain = urlparse(data.callback).netloc
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
|
||||||
try:
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
if r.is_error:
|
|
||||||
raise httpx.ConnectError("LNURL callback connection error")
|
|
||||||
r.raise_for_status()
|
|
||||||
except (httpx.HTTPError, ssl.SSLError) as exc:
|
|
||||||
logger.warning(exc)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Failed to connect to {domain}.",
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
params = json.loads(r.text)
|
|
||||||
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}."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
extra = {}
|
|
||||||
if params.get("successAction"):
|
|
||||||
extra["success_action"] = params["successAction"]
|
|
||||||
if data.comment:
|
|
||||||
extra["comment"] = data.comment
|
|
||||||
if data.unit and data.unit != "sat":
|
|
||||||
extra["fiat_currency"] = data.unit
|
|
||||||
extra["fiat_amount"] = data.amount / 1000
|
|
||||||
assert data.description is not None, "description is required"
|
|
||||||
|
|
||||||
payment = await pay_invoice(
|
|
||||||
wallet_id=wallet.wallet.id,
|
|
||||||
payment_request=params["pr"],
|
|
||||||
description=data.description,
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return payment
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: refactor this route into a public and admin one
|
# TODO: refactor this route into a public and admin one
|
||||||
@payment_router.get("/{payment_hash}")
|
@payment_router.get("/{payment_hash}")
|
||||||
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||||
@@ -403,7 +318,7 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
|||||||
payment_str = data.data
|
payment_str = data.data
|
||||||
try:
|
try:
|
||||||
if payment_str[:5] == "LNURL":
|
if payment_str[:5] == "LNURL":
|
||||||
url = str(lnurl_decode(payment_str))
|
url = str(url_decode(payment_str))
|
||||||
return JSONResponse({"domain": url})
|
return JSONResponse({"domain": url})
|
||||||
else:
|
else:
|
||||||
invoice = bolt11.decode(payment_str)
|
invoice = bolt11.decode(payment_str)
|
||||||
@@ -416,57 +331,32 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK)
|
@payment_router.post("/settle")
|
||||||
async def api_payment_pay_with_nfc(
|
async def api_payments_settle(
|
||||||
payment_request: str,
|
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||||
lnurl_data: PayLnurlWData,
|
) -> InvoiceResponse:
|
||||||
) -> JSONResponse:
|
payment_hash = sha256(bytes.fromhex(data.preimage)).hexdigest()
|
||||||
lnurl = lnurl_data.lnurl_w.lower()
|
payment = await get_standalone_payment(
|
||||||
|
payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
||||||
|
)
|
||||||
|
if not payment:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Payment does not exist or does not belong to this wallet.",
|
||||||
|
)
|
||||||
|
return await settle_hold_invoice(payment, data.preimage)
|
||||||
|
|
||||||
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
|
|
||||||
url = lnurl.replace("lnurlw://", "https://")
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
@payment_router.post("/cancel")
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
async def api_payments_cancel(
|
||||||
try:
|
data: CancelInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||||
check_callback_url(url)
|
) -> InvoiceResponse:
|
||||||
lnurl_req = await client.get(url, timeout=10)
|
payment = await get_standalone_payment(
|
||||||
if lnurl_req.is_error:
|
data.payment_hash, incoming=True, wallet_id=key_type.wallet.id
|
||||||
return JSONResponse(
|
)
|
||||||
{"success": False, "detail": "Error loading LNURL request"}
|
if not payment:
|
||||||
)
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
lnurl_res = lnurl_req.json()
|
detail="Payment does not exist or does not belong to this wallet.",
|
||||||
|
)
|
||||||
if lnurl_res.get("status") == "ERROR":
|
return await cancel_hold_invoice(payment)
|
||||||
return JSONResponse({"success": False, "detail": lnurl_res["reason"]})
|
|
||||||
|
|
||||||
if lnurl_res.get("tag") != "withdrawRequest":
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": "Invalid LNURL-withdraw"}
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_url = lnurl_res["callback"]
|
|
||||||
k1 = lnurl_res["k1"]
|
|
||||||
|
|
||||||
callback_req = await client.get(
|
|
||||||
callback_url,
|
|
||||||
params={"k1": k1, "pr": payment_request},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if callback_req.is_error:
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": "Error loading callback request"}
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_res = callback_req.json()
|
|
||||||
|
|
||||||
if callback_res.get("status") == "ERROR":
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": callback_res["reason"]}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return JSONResponse({"success": True, "detail": callback_res})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return JSONResponse({"success": False, "detail": f"Unexpected error: {e}"})
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from lnbits.core.models.notifications import NotificationType
|
|||||||
from lnbits.core.models.users import Account
|
from lnbits.core.models.users import Account
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
enqueue_notification,
|
enqueue_admin_notification,
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -187,7 +187,8 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
reset_data = ["reset", user_id, int(time.time())]
|
reset_data = ["reset", user_id, int(time.time())]
|
||||||
reset_data_json = json.dumps(reset_data, separators=(",", ":"), ensure_ascii=False)
|
reset_data_json = json.dumps(reset_data, separators=(",", ":"), ensure_ascii=False)
|
||||||
reset_key = encrypt_internal_message(reset_data_json)
|
reset_key = encrypt_internal_message(reset_data_json)
|
||||||
assert reset_key, "Cannot generate reset key."
|
if not reset_key:
|
||||||
|
raise ValueError("Cannot generate reset key.")
|
||||||
reset_key_b64 = base64.b64encode(reset_key.encode()).decode()
|
reset_key_b64 = base64.b64encode(reset_key.encode()).decode()
|
||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
@@ -320,7 +321,7 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||||
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||||
enqueue_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.balance_update,
|
NotificationType.balance_update,
|
||||||
{
|
{
|
||||||
"amount": int(data.amount),
|
"amount": int(data.amount),
|
||||||
|
|||||||
+4
-4
@@ -252,7 +252,7 @@ class Connection(Compat):
|
|||||||
{clause}
|
{clause}
|
||||||
{group_by_string}
|
{group_by_string}
|
||||||
) as count
|
) as count
|
||||||
""",
|
""", # noqa: S608
|
||||||
parsed_values,
|
parsed_values,
|
||||||
)
|
)
|
||||||
row = result.mappings().first()
|
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)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields = ", ".join([f'"{key}"' for key in keys])
|
fields = ", ".join([f'"{key}"' for key in keys])
|
||||||
values = ", ".join(placeholders)
|
values = ", ".join(placeholders)
|
||||||
return f"INSERT INTO {table_name} ({fields}) VALUES ({values})"
|
return f"INSERT INTO {table_name} ({fields}) VALUES ({values})" # noqa: S608
|
||||||
|
|
||||||
|
|
||||||
def update_query(
|
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)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields.append(f'"{field}" = {placeholder}')
|
fields.append(f'"{field}" = {placeholder}')
|
||||||
query = ", ".join(fields)
|
query = ", ".join(fields)
|
||||||
return f"UPDATE {table_name} SET {query} {where}"
|
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||||
|
|
||||||
|
|
||||||
def model_to_dict(model: BaseModel) -> dict:
|
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)
|
return dict_to_model(_subdict, model)
|
||||||
|
|
||||||
|
|
||||||
def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
|
def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
||||||
"""
|
"""
|
||||||
Convert a dictionary with JSON-encoded nested models to a Pydantic model
|
Convert a dictionary with JSON-encoded nested models to a Pydantic model
|
||||||
:param _dict: Dictionary from database
|
:param _dict: Dictionary from database
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def render_html_error(request: Request, exc: Exception) -> Optional[Response]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_exception_handlers(app: FastAPI):
|
def register_exception_handlers(app: FastAPI): # noqa: C901
|
||||||
"""Register exception handlers for the FastAPI app"""
|
"""Register exception handlers for the FastAPI app"""
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
|
|||||||
+11
-4
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import importlib
|
import importlib
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.fiat.base import FiatProvider
|
from lnbits.fiat.base import FiatProvider
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ class FiatProviderType(Enum):
|
|||||||
stripe = "StripeWallet"
|
stripe = "StripeWallet"
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_provider(name: str) -> FiatProvider:
|
async def get_fiat_provider(name: str) -> FiatProvider | None:
|
||||||
if name not in FiatProviderType.__members__:
|
if name not in FiatProviderType.__members__:
|
||||||
raise ValueError(f"Fiat provider '{name}' is not supported.")
|
raise ValueError(f"Fiat provider '{name}' is not supported.")
|
||||||
|
|
||||||
@@ -27,13 +29,18 @@ async def get_fiat_provider(name: str) -> FiatProvider:
|
|||||||
del fiat_providers[name]
|
del fiat_providers[name]
|
||||||
else:
|
else:
|
||||||
return fiat_provider
|
return fiat_provider
|
||||||
fiat_providers[name] = _init_fiat_provider(FiatProviderType[name])
|
_provider = _init_fiat_provider(FiatProviderType[name])
|
||||||
|
if not _provider:
|
||||||
|
return None
|
||||||
|
|
||||||
|
fiat_providers[name] = _provider
|
||||||
return fiat_providers[name]
|
return fiat_providers[name]
|
||||||
|
|
||||||
|
|
||||||
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider:
|
def _init_fiat_provider(fiat_provider: FiatProviderType) -> FiatProvider | None:
|
||||||
if not settings.is_fiat_provider_enabled(fiat_provider.name):
|
if not settings.is_fiat_provider_enabled(fiat_provider.name):
|
||||||
raise ValueError(f"Fiat provider '{fiat_provider.name}' not enabled.")
|
logger.warning(f"Fiat provider '{fiat_provider.name}' not enabled.")
|
||||||
|
return None
|
||||||
provider_constructor = getattr(fiat_module, fiat_provider.value)
|
provider_constructor = getattr(fiat_module, fiat_provider.value)
|
||||||
return provider_constructor()
|
return provider_constructor()
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -16,7 +16,6 @@ from packaging import version
|
|||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
from lnbits.nodes import get_node_class
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
@@ -83,8 +82,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and get_node_class() is not None,
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
"LNBITS_NODE_UI_AVAILABLE": get_node_class() is not None,
|
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
@@ -100,11 +99,8 @@ def template_renderer(additional_folders: Optional[list] = None) -> Jinja2Templa
|
|||||||
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
"LNBITS_DENOMINATION": (
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
settings.lnbits_denomination
|
"has_holdinvoice": settings.has_holdinvoice,
|
||||||
if settings.lnbits_denomination == "FakeWallet"
|
|
||||||
else "sats"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
@@ -306,8 +302,11 @@ def check_callback_url(url: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_url(url, save_path):
|
def download_url(url: str, save_path: Path):
|
||||||
with request.urlopen(url, timeout=60) as dl_file:
|
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
|
||||||
with open(save_path, "wb") as out_file:
|
with open(save_path, "wb") as out_file:
|
||||||
out_file.write(dl_file.read())
|
out_file.write(dl_file.read())
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
from http import HTTPStatus
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from fastapi import HTTPException, Request, Response
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from fastapi.routing import APIRoute
|
|
||||||
from lnurl import LnurlErrorResponse, decode, encode, handle
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
|
||||||
|
|
||||||
|
|
||||||
class LnurlErrorResponseHandler(APIRoute):
|
|
||||||
"""
|
|
||||||
Custom APIRoute class to handle LNURL errors.
|
|
||||||
LNURL errors always return with status 200 and
|
|
||||||
a JSON response with `status="ERROR"` and a `reason` key.
|
|
||||||
Helps to catch HTTPException and return a valid lnurl error response
|
|
||||||
|
|
||||||
Example:
|
|
||||||
withdraw_lnurl_router = APIRouter(prefix="/api/v1/lnurl")
|
|
||||||
withdraw_lnurl_router.route_class = LnurlErrorResponseHandler
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_route_handler(self) -> Callable:
|
|
||||||
original_route_handler = super().get_route_handler()
|
|
||||||
|
|
||||||
async def lnurl_route_handler(request: Request) -> Response:
|
|
||||||
try:
|
|
||||||
response = await original_route_handler(request)
|
|
||||||
return response
|
|
||||||
except (InvoiceError, PaymentError) as exc:
|
|
||||||
logger.debug(f"Wallet Error: {exc}")
|
|
||||||
response = JSONResponse(
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
content={"status": "ERROR", "reason": f"{exc.message}"},
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
except HTTPException as exc:
|
|
||||||
logger.debug(f"HTTPException: {exc}")
|
|
||||||
response = JSONResponse(
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
content={"status": "ERROR", "reason": f"{exc.detail}"},
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error("Unknown Error:", exc)
|
|
||||||
response = JSONResponse(
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
content={
|
|
||||||
"status": "ERROR",
|
|
||||||
"reason": f"UNKNOWN ERROR: {exc!s}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return lnurl_route_handler
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"LnurlErrorResponse",
|
|
||||||
"LnurlErrorResponseHandler",
|
|
||||||
"decode",
|
|
||||||
"encode",
|
|
||||||
"handle",
|
|
||||||
]
|
|
||||||
@@ -131,7 +131,6 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
assert response
|
|
||||||
return response
|
return response
|
||||||
finally:
|
finally:
|
||||||
if request_details:
|
if request_details:
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .base import Node
|
|
||||||
|
|
||||||
|
|
||||||
def get_node_class() -> Optional[Node]:
|
|
||||||
return NODE
|
|
||||||
|
|
||||||
|
|
||||||
def set_node_class(node: Node):
|
|
||||||
global NODE
|
|
||||||
NODE = node
|
|
||||||
|
|
||||||
|
|
||||||
NODE: Optional[Node] = None
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ from httpx import HTTPStatusError
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.nodes import Node
|
|
||||||
from lnbits.nodes.base import (
|
from lnbits.nodes.base import (
|
||||||
ChannelBalance,
|
ChannelBalance,
|
||||||
ChannelPoint,
|
ChannelPoint,
|
||||||
ChannelState,
|
ChannelState,
|
||||||
ChannelStats,
|
ChannelStats,
|
||||||
|
Node,
|
||||||
NodeChannel,
|
NodeChannel,
|
||||||
NodeFees,
|
NodeFees,
|
||||||
NodeInfoResponse,
|
NodeInfoResponse,
|
||||||
|
|||||||
+45
-7
@@ -201,10 +201,11 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
if r.find_in_conflict(ext_redirect_paths)
|
if r.find_in_conflict(ext_redirect_paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert len(existing_redirects) == 0, (
|
if len(existing_redirects) != 0:
|
||||||
f"Cannot redirect for extension '{ext_id}'."
|
raise ValueError(
|
||||||
f" Already mapped by {existing_redirects}."
|
f"Cannot redirect for extension '{ext_id}'."
|
||||||
)
|
f" Already mapped by {existing_redirects}."
|
||||||
|
)
|
||||||
|
|
||||||
self._remove_extension_redirects(ext_id)
|
self._remove_extension_redirects(ext_id)
|
||||||
self.lnbits_extensions_redirects += ext_redirect_paths
|
self.lnbits_extensions_redirects += ext_redirect_paths
|
||||||
@@ -255,7 +256,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||||
lnbits_ad_space_title: str = Field(default="Supported by")
|
lnbits_ad_space_title: str = Field(default="Supported by")
|
||||||
lnbits_ad_space: str = Field(
|
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://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://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"
|
||||||
) # sneaky sneaky
|
) # sneaky sneaky
|
||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
@@ -305,7 +306,7 @@ class FeeSettings(LNbitsSettings):
|
|||||||
return 0
|
return 0
|
||||||
reserve_min = self.lnbits_reserve_fee_min
|
reserve_min = self.lnbits_reserve_fee_min
|
||||||
reserve_percent = self.lnbits_reserve_fee_percent
|
reserve_percent = self.lnbits_reserve_fee_percent
|
||||||
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
return max(int(reserve_min), int(abs(amount_msat) * reserve_percent / 100.0))
|
||||||
|
|
||||||
|
|
||||||
class ExchangeProvidersSettings(LNbitsSettings):
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
@@ -431,6 +432,18 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
default=1_000_000, ge=0
|
default=1_000_000, ge=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_nostr_notifications_configured(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.lnbits_nostr_notifications_enabled
|
||||||
|
and self.lnbits_nostr_notifications_private_key is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_telegram_notifications_configured(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.lnbits_telegram_notifications_enabled
|
||||||
|
and self.lnbits_telegram_notifications_access_token is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
@@ -448,6 +461,18 @@ class ClicheFundingSource(LNbitsSettings):
|
|||||||
cliche_endpoint: str | None = Field(default=None)
|
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):
|
class CoreLightningFundingSource(LNbitsSettings):
|
||||||
corelightning_rpc: str | None = Field(default=None)
|
corelightning_rpc: str | None = Field(default=None)
|
||||||
corelightning_pay_command: str = Field(default="pay")
|
corelightning_pay_command: str = Field(default="pay")
|
||||||
@@ -549,6 +574,12 @@ class BreezSdkFundingSource(LNbitsSettings):
|
|||||||
breez_use_trampoline: bool = Field(default=True)
|
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):
|
class BoltzFundingSource(LNbitsSettings):
|
||||||
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
||||||
boltz_client_macaroon: str | None = Field(default=None)
|
boltz_client_macaroon: str | None = Field(default=None)
|
||||||
@@ -601,6 +632,7 @@ class FundingSourcesSettings(
|
|||||||
FakeWalletFundingSource,
|
FakeWalletFundingSource,
|
||||||
LNbitsFundingSource,
|
LNbitsFundingSource,
|
||||||
ClicheFundingSource,
|
ClicheFundingSource,
|
||||||
|
CLNRestFundingSource,
|
||||||
CoreLightningFundingSource,
|
CoreLightningFundingSource,
|
||||||
CoreLightningRestFundingSource,
|
CoreLightningRestFundingSource,
|
||||||
EclairFundingSource,
|
EclairFundingSource,
|
||||||
@@ -618,6 +650,7 @@ class FundingSourcesSettings(
|
|||||||
NWCFundingSource,
|
NWCFundingSource,
|
||||||
BreezSdkFundingSource,
|
BreezSdkFundingSource,
|
||||||
StrikeFundingSource,
|
StrikeFundingSource,
|
||||||
|
BreezLiquidSdkFundingSource,
|
||||||
):
|
):
|
||||||
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
@@ -676,7 +709,7 @@ class NodeUISettings(LNbitsSettings):
|
|||||||
|
|
||||||
class AuthMethods(Enum):
|
class AuthMethods(Enum):
|
||||||
user_id_only = "user-id-only"
|
user_id_only = "user-id-only"
|
||||||
username_and_password = "username-password"
|
username_and_password = "username-password" # noqa: S105
|
||||||
nostr_auth_nip98 = "nostr-auth-nip98"
|
nostr_auth_nip98 = "nostr-auth-nip98"
|
||||||
google_auth = "google-auth"
|
google_auth = "google-auth"
|
||||||
github_auth = "github-auth"
|
github_auth = "github-auth"
|
||||||
@@ -924,6 +957,8 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
"BoltzWallet",
|
"BoltzWallet",
|
||||||
"BlinkWallet",
|
"BlinkWallet",
|
||||||
"BreezSdkWallet",
|
"BreezSdkWallet",
|
||||||
|
"BreezLiquidSdkWallet",
|
||||||
|
"CLNRestWallet",
|
||||||
"CoreLightningRestWallet",
|
"CoreLightningRestWallet",
|
||||||
"CoreLightningWallet",
|
"CoreLightningWallet",
|
||||||
"EclairWallet",
|
"EclairWallet",
|
||||||
@@ -964,6 +999,9 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
|
|
||||||
server_startup_time: int = Field(default=time())
|
server_startup_time: int = Field(default=time())
|
||||||
|
|
||||||
|
has_holdinvoice: bool = Field(default=False)
|
||||||
|
has_nodemanager: bool = Field(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lnbits_server_up_time(self) -> str:
|
def lnbits_server_up_time(self) -> str:
|
||||||
up_time = int(time() - self.server_startup_time)
|
up_time = int(time() - self.server_startup_time)
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -52,6 +52,7 @@ window.localisation.en = {
|
|||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
wallet_name: 'Wallet name',
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
|
exclude_wallets: 'Exclude Wallets',
|
||||||
add_wallet: 'Add wallet',
|
add_wallet: 'Add wallet',
|
||||||
add_new_wallet: 'Add a new wallet',
|
add_new_wallet: 'Add a new wallet',
|
||||||
pin_wallet: 'Pin wallet',
|
pin_wallet: 'Pin wallet',
|
||||||
@@ -101,6 +102,11 @@ window.localisation.en = {
|
|||||||
memo: 'Memo',
|
memo: 'Memo',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
path: 'Path',
|
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_processing: 'Processing payment...',
|
payment_processing: 'Processing payment...',
|
||||||
payment_successful: 'Payment successful!',
|
payment_successful: 'Payment successful!',
|
||||||
@@ -171,7 +177,17 @@ window.localisation.en = {
|
|||||||
extension_required_lnbits_version: 'This release requires LNbits version',
|
extension_required_lnbits_version: 'This release requires LNbits version',
|
||||||
min_version: 'Minimum (included)',
|
min_version: 'Minimum (included)',
|
||||||
max_version: 'Maximum (excluded)',
|
max_version: 'Maximum (excluded)',
|
||||||
|
preimage: 'Preimage',
|
||||||
|
preimage_hint: 'Preimage to settle the hold invoice',
|
||||||
|
hold_invoice: 'Hold Invoice',
|
||||||
|
hold_invoice_description:
|
||||||
|
'This invoice is on hold and requires a preimage to settle.',
|
||||||
payment_hash: 'Payment Hash',
|
payment_hash: 'Payment Hash',
|
||||||
|
invoice_cancelled: 'Invoice Cancelled',
|
||||||
|
invoice_settled: 'Invoice Settled',
|
||||||
|
hold_invoice_payment_hash: 'Payment hash for hold invoice (optional)',
|
||||||
|
settle_invoice: 'Settle Invoice',
|
||||||
|
cancel_invoice: 'Cancel Invoice',
|
||||||
fee: 'Fee',
|
fee: 'Fee',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
amount_limits: 'Amount Limits',
|
amount_limits: 'Amount Limits',
|
||||||
@@ -214,6 +230,9 @@ window.localisation.en = {
|
|||||||
notifications_nostr_private_key: 'Nostr Private Key',
|
notifications_nostr_private_key: 'Nostr Private Key',
|
||||||
notifications_nostr_private_key_desc:
|
notifications_nostr_private_key_desc:
|
||||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||||
|
notifications_nostr_identifier: 'Nostr Identifier',
|
||||||
|
notifications_nostr_identifier_desc:
|
||||||
|
'Nip5 identifier to send notifications to',
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to',
|
'List of identifiers to send notifications to',
|
||||||
@@ -223,9 +242,10 @@ window.localisation.en = {
|
|||||||
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
||||||
notifications_telegram_access_token: 'Access Token',
|
notifications_telegram_access_token: 'Access Token',
|
||||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||||
notifications_chat_id: 'Chat ID',
|
notifications_chat_id: 'Telegram Chat ID',
|
||||||
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
notifications_chat_id_desc: 'Telegram Chat ID to send the notifications to',
|
||||||
|
notifications_excluded_wallets_desc:
|
||||||
|
'Do not send notifications for these wallets',
|
||||||
notifications_email_config: 'Email Configuration',
|
notifications_email_config: 'Email Configuration',
|
||||||
notifications_enable_email: 'Enable Email',
|
notifications_enable_email: 'Enable Email',
|
||||||
notifications_enable_email_desc: 'Send notifications over email',
|
notifications_enable_email_desc: 'Send notifications over email',
|
||||||
@@ -629,5 +649,7 @@ window.localisation.en = {
|
|||||||
'Signing secret for the webhook. Messages will be signed with this secret.',
|
'Signing secret for the webhook. Messages will be signed with this secret.',
|
||||||
callback_success_url: 'Callback Success URL',
|
callback_success_url: 'Callback Success URL',
|
||||||
callback_success_url_hint:
|
callback_success_url_hint:
|
||||||
'The user will be redirected to this URL after the payment is successful'
|
'The user will be redirected to this URL after the payment is successful',
|
||||||
|
connected: 'Connected',
|
||||||
|
not_connected: 'Not Connected'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ window.localisation.fi = {
|
|||||||
active_channels: 'Aktiivisia kanavia',
|
active_channels: 'Aktiivisia kanavia',
|
||||||
connect_peer: 'Yhdistä naapuriin',
|
connect_peer: 'Yhdistä naapuriin',
|
||||||
connect: 'Yhdistä',
|
connect: 'Yhdistä',
|
||||||
|
reconnect: 'Uudista yhteys',
|
||||||
open_channel: 'Avaa kanava',
|
open_channel: 'Avaa kanava',
|
||||||
open: 'Avaa',
|
open: 'Avaa',
|
||||||
close_channel: 'Sulje kanava',
|
close_channel: 'Sulje kanava',
|
||||||
@@ -51,14 +52,18 @@ window.localisation.fi = {
|
|||||||
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voit lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
||||||
access_wallet_on_mobile: 'Mobiili käyttö',
|
access_wallet_on_mobile: 'Mobiili käyttö',
|
||||||
wallet: 'Lompakko:',
|
wallet: 'Lompakko:',
|
||||||
|
wallet_name: 'Lompakon nimi',
|
||||||
wallets: 'Lompakot',
|
wallets: 'Lompakot',
|
||||||
add_wallet: 'Lisää lompakko',
|
add_wallet: 'Lisää lompakko',
|
||||||
|
add_new_wallet: 'Lisää uusi lompakko',
|
||||||
|
pin_wallet: 'Kiinnitä lompakko',
|
||||||
delete_wallet: 'Poista lompakko',
|
delete_wallet: 'Poista lompakko',
|
||||||
delete_wallet_desc:
|
delete_wallet_desc:
|
||||||
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
|
'Lompakko poistetaan pysyvästi. Siirrä lompakosta varat ennalta muualle, sillä tämä toiminto on PERUUTTAMATON!',
|
||||||
rename_wallet: 'Nimeä lompakko uudelleen',
|
rename_wallet: 'Nimeä lompakko uudelleen',
|
||||||
update_name: 'Tallenna',
|
update_name: 'Tallenna',
|
||||||
fiat_tracking: 'Käytettävä valuutta',
|
fiat_tracking: 'Käytettävä valuutta',
|
||||||
|
fiat_providers: 'Valuutan välittäjät',
|
||||||
currency: 'Valuutta',
|
currency: 'Valuutta',
|
||||||
update_currency: 'Tallenna',
|
update_currency: 'Tallenna',
|
||||||
press_to_claim: 'Lunasta varat painamalla tästä',
|
press_to_claim: 'Lunasta varat painamalla tästä',
|
||||||
@@ -122,6 +127,7 @@ window.localisation.fi = {
|
|||||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||||
created: 'Luotu',
|
created: 'Luotu',
|
||||||
search_extensions: 'Etsi laajennuksia',
|
search_extensions: 'Etsi laajennuksia',
|
||||||
|
search_wallets: 'Etsi lompakkoa',
|
||||||
extension_sources: 'Laajennuslähteet',
|
extension_sources: 'Laajennuslähteet',
|
||||||
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
ext_sources_hint: 'Lähteet joista laajennuksia voi ladata',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
@@ -134,6 +140,7 @@ window.localisation.fi = {
|
|||||||
uninstall: 'Poista',
|
uninstall: 'Poista',
|
||||||
drop_db: 'Poista tiedot',
|
drop_db: 'Poista tiedot',
|
||||||
enable: 'Ota käyttöön',
|
enable: 'Ota käyttöön',
|
||||||
|
enabled: 'Käytössä',
|
||||||
pay_to_enable: 'Maksa ottaaksesi käyttöön',
|
pay_to_enable: 'Maksa ottaaksesi käyttöön',
|
||||||
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
|
enable_extension_details: 'Ota laajennus käyttöön tälle käyttäjälle',
|
||||||
disable: 'Poista käytöstä',
|
disable: 'Poista käytöstä',
|
||||||
@@ -165,12 +172,33 @@ window.localisation.fi = {
|
|||||||
payment_hash: 'Maksun tiiviste',
|
payment_hash: 'Maksun tiiviste',
|
||||||
fee: 'Kulu',
|
fee: 'Kulu',
|
||||||
amount: 'Määrä',
|
amount: 'Määrä',
|
||||||
|
amount_limits: 'Määrien rajat',
|
||||||
amount_sats: 'Määrä (sat)',
|
amount_sats: 'Määrä (sat)',
|
||||||
|
faucest_wallet: 'Faucet Wallet',
|
||||||
|
faucest_wallet_desc_1:
|
||||||
|
'Each time a payment is confirmed by the {provider} provider funds will be subtracted from this wallet.',
|
||||||
|
faucest_wallet_desc_2:
|
||||||
|
'This helps monitor all {provider} payments and their status.',
|
||||||
|
faucest_wallet_desc_3:
|
||||||
|
'This wallet must be topped up with the amount of sats that the admin is willing to offer in exchange for the fiat currency.',
|
||||||
|
faucest_wallet_desc_4:
|
||||||
|
'If this wallet is configured, but is empty, the {provider} payments will not be processed.',
|
||||||
|
faucest_wallet_desc_5:
|
||||||
|
'This wallet can eventually get to a negative balance if parallel fiat payments are made.',
|
||||||
|
faucest_wallet_id: 'Faucet Wallet ID (optional)',
|
||||||
|
faucest_wallet_id_hint:
|
||||||
|
'Wallet ID to use for the faucet. It will be used to send the funds to the user.',
|
||||||
tag: 'Tunniste',
|
tag: 'Tunniste',
|
||||||
unit: 'Yksikkö',
|
unit: 'Yksikkö',
|
||||||
description: 'Kuvaus',
|
description: 'Kuvaus',
|
||||||
expiry: 'Vanhenee',
|
expiry: 'Vanhenee',
|
||||||
webhook: 'Webhook',
|
webhook: 'Webhook',
|
||||||
|
webhook_url: 'Webhook URL',
|
||||||
|
webhook_url_hint:
|
||||||
|
'Webhook URL to send the payment details to. It will be called when the payment is completed.',
|
||||||
|
webhook_events_list: 'The following events must be supported by the webhook:',
|
||||||
|
webhook_stripe_description:
|
||||||
|
'One the stripe side you must configure a webhook with a URL that points to your LNbits server.',
|
||||||
payment_proof: 'Maksun varmenne',
|
payment_proof: 'Maksun varmenne',
|
||||||
update: 'Päivitä',
|
update: 'Päivitä',
|
||||||
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
update_available: 'Saatavilla on päivitys {version}-versioon!',
|
||||||
@@ -262,6 +290,7 @@ window.localisation.fi = {
|
|||||||
notification_source_label:
|
notification_source_label:
|
||||||
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
'Lähde-URL (käytä ainoastaan LNbits:iä tai muuta luotettavaa lähdettä)',
|
||||||
more: 'näytä lisää',
|
more: 'näytä lisää',
|
||||||
|
more_count: 'näytä {count} lisää',
|
||||||
less: 'supista',
|
less: 'supista',
|
||||||
releases: 'Julkaisut',
|
releases: 'Julkaisut',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
@@ -276,7 +305,7 @@ window.localisation.fi = {
|
|||||||
callback_url_rules: 'Callback URL -säännöt',
|
callback_url_rules: 'Callback URL -säännöt',
|
||||||
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
enter_callback_url_rule: 'Anna URL-sääntö regex-muodossa ja paina enter',
|
||||||
callback_url_rule_hint:
|
callback_url_rule_hint:
|
||||||
'Callback URL:it (kuten LNURL) tarkistetaan näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
'Callback URL:it (kuten LNURL) tarkistetaan kaikkien näiden sääntöjen mukaisesti. Jos sääntöjä ei ole määritetty, kaikki URL:it ovat sallittuja.',
|
||||||
wallet_limiter: 'Lompakon käyttörajoitin',
|
wallet_limiter: 'Lompakon käyttörajoitin',
|
||||||
wallet_config: 'Wallet Config',
|
wallet_config: 'Wallet Config',
|
||||||
wallet_charts: 'Wallet Charts',
|
wallet_charts: 'Wallet Charts',
|
||||||
@@ -301,6 +330,9 @@ window.localisation.fi = {
|
|||||||
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
login_with_user_id: 'Kirjaudu käyttäjä-ID:llä',
|
||||||
or: 'tai',
|
or: 'tai',
|
||||||
create_new_wallet: 'Avaa uusi lompakko',
|
create_new_wallet: 'Avaa uusi lompakko',
|
||||||
|
delete_all_wallets: 'Poista kaikki lompakot',
|
||||||
|
confirm_delete_all_wallets:
|
||||||
|
'Oletko todellakin varma, että haluat poistaa käyttäjältä KAIKKI lompakot?',
|
||||||
login_to_account: 'Kirjaudu käyttäjänimellä',
|
login_to_account: 'Kirjaudu käyttäjänimellä',
|
||||||
create_account: 'Luo tili',
|
create_account: 'Luo tili',
|
||||||
account_settings: 'Tilin asetukset',
|
account_settings: 'Tilin asetukset',
|
||||||
@@ -309,7 +341,7 @@ window.localisation.fi = {
|
|||||||
signin_with_nostr: 'Kirjaudu Nostr:lla',
|
signin_with_nostr: 'Kirjaudu Nostr:lla',
|
||||||
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
signin_with_google: 'Kirjaudu Google-tunnuksella',
|
||||||
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
signin_with_github: 'Kirjaudu GitHub-tunnuksella',
|
||||||
signin_with_keycloak: 'Kirjaudu Keycloak-tunnuksella',
|
signin_with_custom_org: 'Kirjaudu {custom_org}-palvelulla',
|
||||||
username_or_email: 'Käyttäjänimi tai sähköposti',
|
username_or_email: 'Käyttäjänimi tai sähköposti',
|
||||||
password: 'Anna uusi salasana',
|
password: 'Anna uusi salasana',
|
||||||
password_config: 'Salasanan määritys',
|
password_config: 'Salasanan määritys',
|
||||||
@@ -318,7 +350,10 @@ window.localisation.fi = {
|
|||||||
change_password: 'Vaihda salasana',
|
change_password: 'Vaihda salasana',
|
||||||
update_credentials: 'Päivitä käyttöoikeustiedot',
|
update_credentials: 'Päivitä käyttöoikeustiedot',
|
||||||
update_pubkey: 'Päivitä julkinen avain',
|
update_pubkey: 'Päivitä julkinen avain',
|
||||||
|
nostr_pubkey_tooltip:
|
||||||
|
'Syötä tämän käyttäjän julkinen Nostr avain (hex arvona)',
|
||||||
set_password: 'Aseta salasana',
|
set_password: 'Aseta salasana',
|
||||||
|
set_password_tooltip: 'Aseta käyttäjätunnukselle salasana',
|
||||||
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
invalid_password: 'Salasanassa tulee olla vähintään kahdeksan merkkiä',
|
||||||
invalid_password_repeat: 'Salasanat eivät täsmää',
|
invalid_password_repeat: 'Salasanat eivät täsmää',
|
||||||
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
reset_key_generated: 'Salasanan vaihtoavain on luotu.',
|
||||||
@@ -327,7 +362,8 @@ window.localisation.fi = {
|
|||||||
register: 'Rekisteröidy',
|
register: 'Rekisteröidy',
|
||||||
username: 'Käyttäjänimi',
|
username: 'Käyttäjänimi',
|
||||||
pubkey: 'Julkinen avain',
|
pubkey: 'Julkinen avain',
|
||||||
user_id: 'Käyttäjä ID',
|
user_id: 'Käyttäjä tunnus',
|
||||||
|
id: 'tunnus',
|
||||||
email: 'Sähköposti',
|
email: 'Sähköposti',
|
||||||
first_name: 'Etunimi',
|
first_name: 'Etunimi',
|
||||||
last_name: 'Sukunimi',
|
last_name: 'Sukunimi',
|
||||||
@@ -343,6 +379,8 @@ window.localisation.fi = {
|
|||||||
back: 'Takaisin',
|
back: 'Takaisin',
|
||||||
logout: 'Poistu',
|
logout: 'Poistu',
|
||||||
look_and_feel: 'Kieli ja värit',
|
look_and_feel: 'Kieli ja värit',
|
||||||
|
endpoint: 'Endpoint',
|
||||||
|
api: 'API',
|
||||||
api_token: 'API Token',
|
api_token: 'API Token',
|
||||||
api_tokens: 'API Tokens',
|
api_tokens: 'API Tokens',
|
||||||
access_control_list: 'Access Control List',
|
access_control_list: 'Access Control List',
|
||||||
@@ -354,11 +392,14 @@ window.localisation.fi = {
|
|||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Kieli',
|
language: 'Kieli',
|
||||||
color_scheme: 'Väriteema',
|
color_scheme: 'Väriteema',
|
||||||
|
visible_wallet_count: 'Näytettävien lompakkojen määrä',
|
||||||
admin_settings: 'Pääkäyttäjän asetukset',
|
admin_settings: 'Pääkäyttäjän asetukset',
|
||||||
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
|
extension_cost: 'Tämä laajennus edellyttää vähintään {cost} sat maksua.',
|
||||||
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
|
extension_paid_sats: 'Olet jo maksanut {paid_sats} satsia.',
|
||||||
release_details_error: 'Ei voi hakea julkaisun tietoja.',
|
release_details_error: 'Ei voi hakea julkaisun tietoja.',
|
||||||
pay_from_wallet: 'Maksa lompakosta',
|
pay_from_wallet: 'Maksa lompakosta',
|
||||||
|
pay_with: 'Maksa {provider}:lla',
|
||||||
|
select_payment_provider: 'Valitse maksun välittäjä',
|
||||||
wallet_required: 'Lompakko *',
|
wallet_required: 'Lompakko *',
|
||||||
show_qr: 'Näytä QR',
|
show_qr: 'Näytä QR',
|
||||||
retry_install: 'Yritä asennusta uudelleen',
|
retry_install: 'Yritä asennusta uudelleen',
|
||||||
@@ -371,6 +412,9 @@ window.localisation.fi = {
|
|||||||
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
|
'{name} -laajennuksen aktivointi edellyttää vähintään {amount} sat maksua.',
|
||||||
hide_empty_wallets: 'Piilota tyhjät lompakot',
|
hide_empty_wallets: 'Piilota tyhjät lompakot',
|
||||||
recheck: 'Tarkista uudelleen',
|
recheck: 'Tarkista uudelleen',
|
||||||
|
check: 'Tarkista',
|
||||||
|
check_connection: 'Tarkista yhteys',
|
||||||
|
check_webhook: 'Tarkista Webhook',
|
||||||
contributors: 'Avustajat',
|
contributors: 'Avustajat',
|
||||||
license: 'Lisenssi',
|
license: 'Lisenssi',
|
||||||
reset_key: 'Vaihda avain',
|
reset_key: 'Vaihda avain',
|
||||||
@@ -447,9 +491,9 @@ window.localisation.fi = {
|
|||||||
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
fee_reserve_percent: 'Kuluvaraus prosentteina',
|
||||||
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
fee_reserve_msats: 'Kuluvaraus milli-sat',
|
||||||
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
|
reserve_fee_in_percent: 'Kuluvaraus prosentteina',
|
||||||
payment_wait_time: 'Maksun odotusaika',
|
payment_wait_time: 'Maksun odotusaika (sekuntia)',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'Kuinka pitkään maksua odotetaan saapuvaksi ennenkuin se merkitään Odotetaan -tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua tms',
|
'Kuinka pitkään maksua odotetaan saapuvaksi, ennen kuin se merkitään Odotetaan-tilaan. Aseta pidemmäksi käytettäessä HODL-laskuja, Boltz-palvelua, tms',
|
||||||
server_management: 'Palvelimen hallinta',
|
server_management: 'Palvelimen hallinta',
|
||||||
base_url: 'Palvelimen URL-osoite',
|
base_url: 'Palvelimen URL-osoite',
|
||||||
base_url_label: 'Palvelun staattinen pohja-URL',
|
base_url_label: 'Palvelun staattinen pohja-URL',
|
||||||
@@ -474,12 +518,16 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
|
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
|
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
||||||
|
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
||||||
currency_settings: 'Valuutta-asetukset',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
default_account_currency: 'Tilin oletusvaluutta',
|
default_account_currency: 'Tilin oletusvaluutta',
|
||||||
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
default_account_currency_hint: 'Kirjanpidon oletusvaluutta',
|
||||||
|
min_incoming_payment_amount: 'Pienin vastaanotettava maksun määrä',
|
||||||
|
min_incoming_payment_amount_desc:
|
||||||
|
'Pienin maksun määrä jolle voi luoda laskun',
|
||||||
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
|
max_incoming_payment_amount: 'Saapuvan maksun enimmäismäärä',
|
||||||
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
|
max_incoming_payment_amount_desc: 'Enimmäismäärä jonka voi laskuttaa',
|
||||||
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
|
max_outgoing_payment_amount: 'Lähtevän maksun enimmäismäärä',
|
||||||
@@ -542,6 +590,8 @@ window.localisation.fi = {
|
|||||||
admin_users_label: 'Käyttäjätunnus',
|
admin_users_label: 'Käyttäjätunnus',
|
||||||
allowed_users: 'Sallitut käyttäjät',
|
allowed_users: 'Sallitut käyttäjät',
|
||||||
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
allowed_users_hint: 'Vain nämä käyttäjät voivat käyttää LNbitsiä',
|
||||||
|
allowed_users_hint_feature:
|
||||||
|
'Ainoastaan nämä käyttäjät voivat käyttää ominaisuutta {feature}',
|
||||||
allowed_users_label: 'Käyttäjätunnus',
|
allowed_users_label: 'Käyttäjätunnus',
|
||||||
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
allow_creation_user: 'Salli uusien käyttäjien luominen',
|
||||||
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
allow_creation_user_desc: 'Etusivulta on mahdollisuus luoda uusia käyttäjiä',
|
||||||
@@ -577,5 +627,12 @@ window.localisation.fi = {
|
|||||||
view_column: 'Näytä lompakot rinnakkain',
|
view_column: 'Näytä lompakot rinnakkain',
|
||||||
filter_payments: 'Suodata maksuja',
|
filter_payments: 'Suodata maksuja',
|
||||||
filter_date: 'Suodata päiväyksellä',
|
filter_date: 'Suodata päiväyksellä',
|
||||||
websocket_example: 'Websocket esimerkki'
|
websocket_example: 'Websocket example',
|
||||||
|
secret_key: 'Secret Key',
|
||||||
|
signing_secret: 'Signing Secret',
|
||||||
|
signing_secret_hint:
|
||||||
|
'Signing secret for the webhook. Messages will be signed with this secret.',
|
||||||
|
callback_success_url: 'Callback Success URL',
|
||||||
|
callback_success_url_hint:
|
||||||
|
'The user will be redirected to this URL after the payment is successful'
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -80,6 +80,11 @@ window.AccountPageLogic = {
|
|||||||
token_id_list: [],
|
token_id_list: [],
|
||||||
allRead: false,
|
allRead: false,
|
||||||
allWrite: false
|
allWrite: false
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
nostr: {
|
||||||
|
identifier: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -396,6 +401,7 @@ window.AccountPageLogic = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||||
|
|||||||
+32
-29
@@ -19,45 +19,47 @@ window.LNbits = {
|
|||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
unit = 'sat',
|
unit = 'sat',
|
||||||
lnurlCallback = null,
|
lnurlWithdraw = null,
|
||||||
fiatProvider = null
|
fiatProvider = null,
|
||||||
|
internalMemo = null,
|
||||||
|
payment_hash = null
|
||||||
) {
|
) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
const data = {
|
||||||
out: false,
|
out: false,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
lnurl_callback: lnurlCallback,
|
|
||||||
unit: unit,
|
unit: unit,
|
||||||
fiat_provider: fiatProvider
|
lnurl_withdraw: lnurlWithdraw,
|
||||||
})
|
fiat_provider: fiatProvider,
|
||||||
|
payment_hash: payment_hash
|
||||||
|
}
|
||||||
|
if (internalMemo) {
|
||||||
|
data.extra = {
|
||||||
|
internal_memo: String(internalMemo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request('post', '/api/v1/payments', wallet.inkey, data)
|
||||||
},
|
},
|
||||||
payInvoice(wallet, bolt11) {
|
payInvoice(wallet, bolt11, internalMemo = null) {
|
||||||
return this.request('post', '/api/v1/payments', wallet.adminkey, {
|
const data = {
|
||||||
out: true,
|
out: true,
|
||||||
bolt11: bolt11
|
bolt11: bolt11
|
||||||
|
}
|
||||||
|
if (internalMemo) {
|
||||||
|
data.extra = {
|
||||||
|
internal_memo: String(internalMemo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
||||||
|
},
|
||||||
|
cancelInvoice(wallet, paymentHash) {
|
||||||
|
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
||||||
|
payment_hash: paymentHash
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl(
|
settleInvoice(wallet, preimage) {
|
||||||
wallet,
|
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
|
||||||
callback,
|
preimage: preimage
|
||||||
description_hash,
|
|
||||||
amount,
|
|
||||||
description = '',
|
|
||||||
comment = '',
|
|
||||||
unit = ''
|
|
||||||
) {
|
|
||||||
return this.request('post', '/api/v1/payments/lnurl', wallet.adminkey, {
|
|
||||||
callback,
|
|
||||||
description_hash,
|
|
||||||
amount,
|
|
||||||
comment,
|
|
||||||
description,
|
|
||||||
unit
|
|
||||||
})
|
|
||||||
},
|
|
||||||
authLnurl(wallet, callback) {
|
|
||||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
|
||||||
callback
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createAccount(name) {
|
createAccount(name) {
|
||||||
@@ -613,6 +615,7 @@ window.windowMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyBackgroundImage() {
|
applyBackgroundImage() {
|
||||||
|
if (this.bgimageChoice == 'null') this.bgimageChoice = ''
|
||||||
if (this.bgimageChoice == '') {
|
if (this.bgimageChoice == '') {
|
||||||
document.body.classList.remove('bg-image')
|
document.body.classList.remove('bg-image')
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -48,6 +48,22 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
|
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',
|
'CoreLightningWallet',
|
||||||
'Core Lightning',
|
'Core Lightning',
|
||||||
@@ -58,7 +74,7 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'CoreLightningRestWallet',
|
'CoreLightningRestWallet',
|
||||||
'Core Lightning Rest',
|
'Core Lightning Rest (legacy)',
|
||||||
{
|
{
|
||||||
corelightning_rest_url: 'Endpoint',
|
corelightning_rest_url: 'Endpoint',
|
||||||
corelightning_rest_cert: 'Certificate',
|
corelightning_rest_cert: 'Certificate',
|
||||||
@@ -219,6 +235,16 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
strike_api_endpoint: 'API Endpoint',
|
strike_api_endpoint: 'API Endpoint',
|
||||||
strike_api_key: 'API Key'
|
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'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,13 @@ window.app.component('payment-list', {
|
|||||||
field: row => row.extra.wallet_fiat_amount
|
field: row => row.extra.wallet_fiat_amount
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
preimage: null,
|
||||||
loading: false
|
loading: false
|
||||||
|
},
|
||||||
|
hodlInvoice: {
|
||||||
|
show: false,
|
||||||
|
payment: null,
|
||||||
|
preimage: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -223,6 +229,35 @@ window.app.component('payment-list', {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
|
showHoldInvoiceDialog(payment) {
|
||||||
|
this.hodlInvoice.show = true
|
||||||
|
this.hodlInvoice.preimage = ''
|
||||||
|
this.hodlInvoice.payment = payment
|
||||||
|
},
|
||||||
|
cancelHoldInvoice(payment_hash) {
|
||||||
|
LNbits.api
|
||||||
|
.cancelInvoice(this.g.wallet, payment_hash)
|
||||||
|
.then(() => {
|
||||||
|
this.update = !this.update
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: this.$t('invoice_cancelled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
|
settleHoldInvoice(preimage) {
|
||||||
|
LNbits.api
|
||||||
|
.settleInvoice(this.g.wallet, preimage)
|
||||||
|
.then(() => {
|
||||||
|
this.update = !this.update
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: this.$t('invoice_settled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(LNbits.utils.notifyApiError)
|
||||||
|
},
|
||||||
paymentTableRowKey(row) {
|
paymentTableRowKey(row) {
|
||||||
return row.payment_hash + row.amount
|
return row.payment_hash + row.amount
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ window.PaymentsPageLogic = {
|
|||||||
searchData: {
|
searchData: {
|
||||||
wallet_id: null,
|
wallet_id: null,
|
||||||
payment_hash: null,
|
payment_hash: null,
|
||||||
memo: null
|
memo: null,
|
||||||
|
internal_memo: null
|
||||||
},
|
},
|
||||||
statusFilters: {
|
statusFilters: {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -60,10 +61,10 @@ window.PaymentsPageLogic = {
|
|||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'fee',
|
name: 'fee_sats',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Fee',
|
label: 'Fee',
|
||||||
field: 'fee',
|
field: 'fee_sats',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,6 +83,14 @@ window.PaymentsPageLogic = {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
max_length: 20
|
max_length: 20
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'internal_memo',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Internal Memo',
|
||||||
|
field: 'internal_memo',
|
||||||
|
sortable: false,
|
||||||
|
max_length: 20
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'wallet_id',
|
name: 'wallet_id',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@@ -166,6 +175,11 @@ window.PaymentsPageLogic = {
|
|||||||
p.extra.wallet_fiat_currency
|
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
|
return p
|
||||||
})
|
})
|
||||||
|
|||||||
+110
-113
@@ -14,6 +14,7 @@ window.WalletPageLogic = {
|
|||||||
request: '',
|
request: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
comment: '',
|
comment: '',
|
||||||
|
internalMemo: null,
|
||||||
unit: 'sat'
|
unit: 'sat'
|
||||||
},
|
},
|
||||||
paymentChecker: null,
|
paymentChecker: null,
|
||||||
@@ -38,7 +39,9 @@ window.WalletPageLogic = {
|
|||||||
fiatProvider: '',
|
fiatProvider: '',
|
||||||
data: {
|
data: {
|
||||||
amount: null,
|
amount: null,
|
||||||
memo: ''
|
memo: '',
|
||||||
|
internalMemo: null,
|
||||||
|
payment_hash: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
invoiceQrCode: '',
|
invoiceQrCode: '',
|
||||||
@@ -138,6 +141,13 @@ window.WalletPageLogic = {
|
|||||||
},
|
},
|
||||||
canPay() {
|
canPay() {
|
||||||
if (!this.parse.invoice) return false
|
if (!this.parse.invoice) return false
|
||||||
|
if (this.parse.invoice.expired) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Invoice has expired',
|
||||||
|
color: 'negative'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
return this.parse.invoice.sat <= this.g.wallet.sat
|
return this.parse.invoice.sat <= this.g.wallet.sat
|
||||||
},
|
},
|
||||||
formattedAmount() {
|
formattedAmount() {
|
||||||
@@ -197,6 +207,8 @@ window.WalletPageLogic = {
|
|||||||
this.receive.paymentHash = null
|
this.receive.paymentHash = null
|
||||||
this.receive.data.amount = null
|
this.receive.data.amount = null
|
||||||
this.receive.data.memo = null
|
this.receive.data.memo = null
|
||||||
|
this.receive.data.internalMemo = null
|
||||||
|
this.receive.data.payment_hash = null
|
||||||
this.receive.unit = this.isFiatPriority
|
this.receive.unit = this.isFiatPriority
|
||||||
? this.g.wallet.currency || 'sat'
|
? this.g.wallet.currency || 'sat'
|
||||||
: 'sat'
|
: 'sat'
|
||||||
@@ -218,6 +230,7 @@ window.WalletPageLogic = {
|
|||||||
window.isSecureContext && navigator.clipboard?.readText !== undefined
|
window.isSecureContext && navigator.clipboard?.readText !== undefined
|
||||||
this.parse.data.request = ''
|
this.parse.data.request = ''
|
||||||
this.parse.data.comment = ''
|
this.parse.data.comment = ''
|
||||||
|
this.parse.data.internalMemo = null
|
||||||
this.parse.data.paymentChecker = null
|
this.parse.data.paymentChecker = null
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
this.focusInput('textArea')
|
this.focusInput('textArea')
|
||||||
@@ -252,8 +265,10 @@ window.WalletPageLogic = {
|
|||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo,
|
this.receive.data.memo,
|
||||||
this.receive.unit,
|
this.receive.unit,
|
||||||
this.receive.lnurl && this.receive.lnurl.callback,
|
this.receive.lnurlWithdraw,
|
||||||
this.receive.fiatProvider
|
this.receive.fiatProvider,
|
||||||
|
this.receive.data.internalMemo,
|
||||||
|
this.receive.data.payment_hash
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.g.updatePayments = !this.g.updatePayments
|
this.g.updatePayments = !this.g.updatePayments
|
||||||
@@ -266,27 +281,29 @@ window.WalletPageLogic = {
|
|||||||
if (!this.receive.lnurl) {
|
if (!this.receive.lnurl) {
|
||||||
this.readNfcTag()
|
this.readNfcTag()
|
||||||
}
|
}
|
||||||
// TODO: lnurl_callback and lnurl_response
|
|
||||||
// WITHDRAW
|
// WITHDRAW
|
||||||
if (response.data.lnurl_response !== null) {
|
if (
|
||||||
if (response.data.lnurl_response === false) {
|
this.receive.lnurl &&
|
||||||
response.data.lnurl_response = `Unable to connect`
|
response.data.extra?.lnurl_response !== null
|
||||||
|
) {
|
||||||
|
if (response.data.extra.lnurl_response === false) {
|
||||||
|
response.data.extra.lnurl_response = `Unable to connect`
|
||||||
}
|
}
|
||||||
|
const domain = this.receive.lnurl.callback.split('/')[2]
|
||||||
if (typeof response.data.lnurl_response === 'string') {
|
if (typeof response.data.extra.lnurl_response === 'string') {
|
||||||
// failure
|
// failure
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
message: `${domain} lnurl-withdraw call failed.`,
|
||||||
caption: response.data.lnurl_response
|
caption: response.data.extra.lnurl_response
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.data.lnurl_response === true) {
|
} else if (response.data.extra.lnurl_response === true) {
|
||||||
// success
|
// success
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 3000,
|
||||||
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
message: `Invoice sent to ${domain}!`,
|
||||||
spinner: true
|
spinner: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -335,32 +352,30 @@ window.WalletPageLogic = {
|
|||||||
},
|
},
|
||||||
lnurlScan() {
|
lnurlScan() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, {
|
||||||
'GET',
|
lnurl: this.parse.data.request
|
||||||
'/api/v1/lnurlscan/' + this.parse.data.request,
|
})
|
||||||
this.g.wallet.adminkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
if (data.status === 'ERROR') {
|
if (data.status === 'ERROR') {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `${data.domain} lnurl call failed.`,
|
message: `lnurl scan failed.`,
|
||||||
caption: data.reason
|
caption: data.reason
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.kind === 'pay') {
|
if (data.tag === 'payRequest') {
|
||||||
this.parse.lnurlpay = Object.freeze(data)
|
this.parse.lnurlpay = Object.freeze(data)
|
||||||
this.parse.data.amount = data.minSendable / 1000
|
this.parse.data.amount = data.minSendable / 1000
|
||||||
} else if (data.kind === 'auth') {
|
} else if (data.tag === 'login') {
|
||||||
this.parse.lnurlauth = Object.freeze(data)
|
this.parse.lnurlauth = Object.freeze(data)
|
||||||
} else if (data.kind === 'withdraw') {
|
} else if (data.tag === 'withdrawRequest') {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
|
this.receive.lnurlWithdraw = Object.freeze(data)
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
this.receive.paymentReq = null
|
this.receive.paymentReq = null
|
||||||
this.receive.paymentHash = null
|
this.receive.paymentHash = null
|
||||||
@@ -370,8 +385,9 @@ window.WalletPageLogic = {
|
|||||||
data.minWithdrawable / 1000,
|
data.minWithdrawable / 1000,
|
||||||
data.maxWithdrawable / 1000
|
data.maxWithdrawable / 1000
|
||||||
]
|
]
|
||||||
|
const domain = data.callback.split('/')[2]
|
||||||
this.receive.lnurl = {
|
this.receive.lnurl = {
|
||||||
domain: data.domain,
|
domain: domain,
|
||||||
callback: data.callback,
|
callback: data.callback,
|
||||||
fixed: data.fixed
|
fixed: data.fixed
|
||||||
}
|
}
|
||||||
@@ -386,19 +402,29 @@ window.WalletPageLogic = {
|
|||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
this.parse.camera.show = false
|
this.parse.camera.show = false
|
||||||
},
|
},
|
||||||
|
isLnurl(req) {
|
||||||
|
return (
|
||||||
|
req.toLowerCase().startsWith('lnurl1') ||
|
||||||
|
req.startsWith('lnurlp://') ||
|
||||||
|
req.startsWith('lnurlw://') ||
|
||||||
|
req.startsWith('lnurlauth://') ||
|
||||||
|
req.match(/[\w.+-~_]+@[\w.+-~_]/)
|
||||||
|
)
|
||||||
|
},
|
||||||
decodeRequest() {
|
decodeRequest() {
|
||||||
this.parse.show = true
|
this.parse.show = true
|
||||||
this.parse.data.request = this.parse.data.request.trim().toLowerCase()
|
this.parse.data.request = this.parse.data.request.trim()
|
||||||
let req = this.parse.data.request
|
const req = this.parse.data.request.toLowerCase()
|
||||||
if (req.startsWith('lightning:')) {
|
if (req.startsWith('lightning:')) {
|
||||||
this.parse.data.request = req.slice(10)
|
this.parse.data.request = this.parse.data.request.slice(10)
|
||||||
} else if (req.startsWith('lnurl:')) {
|
} else if (req.startsWith('lnurl:')) {
|
||||||
this.parse.data.request = req.slice(6)
|
this.parse.data.request = this.parse.data.request.slice(6)
|
||||||
} else if (req.includes('lightning=lnurl1')) {
|
} else if (req.includes('lightning=lnurl1')) {
|
||||||
this.parse.data.request = req.split('lightning=')[1].split('&')[0]
|
this.parse.data.request = this.parse.data.request
|
||||||
|
.split('lightning=')[1]
|
||||||
|
.split('&')[0]
|
||||||
}
|
}
|
||||||
req = this.parse.data.request
|
if (this.isLnurl(this.parse.data.request)) {
|
||||||
if (req.startsWith('lnurl1') || req.match(/[\w.+-~_]+@[\w.+-~_]/)) {
|
|
||||||
this.lnurlScan()
|
this.lnurlScan()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -477,7 +503,11 @@ window.WalletPageLogic = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.payInvoice(this.g.wallet, this.parse.data.request)
|
.payInvoice(
|
||||||
|
this.g.wallet,
|
||||||
|
this.parse.data.request,
|
||||||
|
this.parse.data.internalMemo
|
||||||
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dismissPaymentMsg()
|
dismissPaymentMsg()
|
||||||
this.updatePayments = !this.updatePayments
|
this.updatePayments = !this.updatePayments
|
||||||
@@ -503,85 +533,49 @@ window.WalletPageLogic = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl() {
|
payLnurl() {
|
||||||
const dismissPaymentMsg = Quasar.Notify.create({
|
|
||||||
timeout: 0,
|
|
||||||
message: 'Processing payment...'
|
|
||||||
})
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.payLnurl(
|
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
||||||
this.g.wallet,
|
res: this.parse.lnurlpay,
|
||||||
this.parse.lnurlpay.callback,
|
unit: this.parse.data.unit,
|
||||||
this.parse.lnurlpay.description_hash,
|
amount: this.parse.data.amount * 1000,
|
||||||
this.parse.data.amount * 1000,
|
comment: this.parse.data.comment,
|
||||||
this.parse.lnurlpay.description.slice(0, 120),
|
internalMemo: this.parse.data.internalMemo
|
||||||
this.parse.data.comment,
|
})
|
||||||
this.parse.data.unit
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
|
if (response.data.extra.success_action) {
|
||||||
clearInterval(this.parse.paymentChecker)
|
const action = JSON.parse(response.data.extra.success_action)
|
||||||
setTimeout(() => {
|
switch (action.tag) {
|
||||||
clearInterval(this.parse.paymentChecker)
|
case 'url':
|
||||||
}, 40000)
|
Quasar.Notify.create({
|
||||||
this.parse.paymentChecker = setInterval(() => {
|
message: `<a target="_blank" style="color: inherit" href="${action.url}">${action.url}</a>`,
|
||||||
LNbits.api
|
caption: action.description,
|
||||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
html: true,
|
||||||
.then(res => {
|
type: 'positive',
|
||||||
if (res.data.paid) {
|
timeout: 0,
|
||||||
dismissPaymentMsg()
|
closeBtn: true
|
||||||
clearInterval(this.parse.paymentChecker)
|
})
|
||||||
// show lnurlpay success action
|
break
|
||||||
const extra = response.data.extra
|
case 'message':
|
||||||
if (extra.success_action) {
|
Quasar.Notify.create({
|
||||||
switch (extra.success_action.tag) {
|
message: action.message,
|
||||||
case 'url':
|
type: 'positive',
|
||||||
Quasar.Notify.create({
|
timeout: 0,
|
||||||
message: `<a target="_blank" style="color: inherit" href="${extra.success_action.url}">${extra.success_action.url}</a>`,
|
closeBtn: true
|
||||||
caption: extra.success_action.description,
|
})
|
||||||
html: true,
|
break
|
||||||
type: 'positive',
|
case 'aes':
|
||||||
timeout: 0,
|
decryptLnurlPayAES(action, response.data.preimage)
|
||||||
closeBtn: true
|
Quasar.Notify.create({
|
||||||
})
|
message: value,
|
||||||
break
|
caption: extra.success_action.description,
|
||||||
case 'message':
|
html: true,
|
||||||
Quasar.Notify.create({
|
type: 'positive',
|
||||||
message: extra.success_action.message,
|
timeout: 0,
|
||||||
type: 'positive',
|
closeBtn: true
|
||||||
timeout: 0,
|
})
|
||||||
closeBtn: true
|
}
|
||||||
})
|
}
|
||||||
break
|
|
||||||
case 'aes':
|
|
||||||
LNbits.api
|
|
||||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
|
||||||
.then(({data: payment}) =>
|
|
||||||
decryptLnurlPayAES(
|
|
||||||
extra.success_action,
|
|
||||||
payment.preimage
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(value => {
|
|
||||||
Quasar.Notify.create({
|
|
||||||
message: value,
|
|
||||||
caption: extra.success_action.description,
|
|
||||||
html: true,
|
|
||||||
type: 'positive',
|
|
||||||
timeout: 0,
|
|
||||||
closeBtn: true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 2000)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
dismissPaymentMsg()
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
authLnurl() {
|
authLnurl() {
|
||||||
@@ -589,9 +583,13 @@ window.WalletPageLogic = {
|
|||||||
timeout: 10,
|
timeout: 10,
|
||||||
message: 'Performing authentication...'
|
message: 'Performing authentication...'
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
|
.request(
|
||||||
|
'post',
|
||||||
|
'/api/v1/lnurlauth',
|
||||||
|
wallet.adminkey,
|
||||||
|
this.parse.lnurlauth
|
||||||
|
)
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
dismissAuthMsg()
|
dismissAuthMsg()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -602,10 +600,9 @@ window.WalletPageLogic = {
|
|||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dismissAuthMsg()
|
|
||||||
if (err.response.data.reason) {
|
if (err.response.data.reason) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: `Authentication failed. ${this.parse.lnurlauth.domain} says:`,
|
message: `Authentication failed. ${this.parse.lnurlauth.callback} says:`,
|
||||||
caption: err.response.data.reason,
|
caption: err.response.data.reason,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+11
-46
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
@@ -11,7 +10,6 @@ from typing import (
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
get_payments,
|
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
update_payment,
|
update_payment,
|
||||||
)
|
)
|
||||||
@@ -153,51 +151,18 @@ def wait_for_paid_invoices(
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
async def check_pending_payments():
|
def run_interval(
|
||||||
"""
|
interval_seconds: int,
|
||||||
check_pending_payments is called during startup to check for pending payments with
|
func: Callable[[], Coroutine],
|
||||||
the backend and also to delete expired invoices. Incoming payments will be
|
) -> Callable[[], Coroutine]:
|
||||||
checked only once, outgoing pending payments will be checked regularly.
|
"""Run a function at a specified interval in seconds, while the server is running"""
|
||||||
"""
|
|
||||||
sleep_time = 60 * 30 # 30 minutes
|
|
||||||
|
|
||||||
while settings.lnbits_running:
|
async def wrapper() -> None:
|
||||||
funding_source = get_funding_source()
|
while settings.lnbits_running:
|
||||||
if funding_source.__class__.__name__ == "VoidWallet":
|
await func()
|
||||||
logger.warning("Task: skipping pending check for VoidWallet")
|
await asyncio.sleep(interval_seconds)
|
||||||
await asyncio.sleep(sleep_time)
|
|
||||||
continue
|
return wrapper
|
||||||
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):
|
async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = False):
|
||||||
|
|||||||
@@ -370,7 +370,12 @@
|
|||||||
v-if="extras.length"
|
v-if="extras.length"
|
||||||
>
|
>
|
||||||
<template v-for="entry in extras">
|
<template v-for="entry in extras">
|
||||||
<q-item v-if="!!entry.value" key="entry.key" class="text-grey-4">
|
<q-item
|
||||||
|
v-if="!!entry.value"
|
||||||
|
key="entry.key"
|
||||||
|
class="text-grey-4"
|
||||||
|
style="white-space: normal; word-break: break-all"
|
||||||
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="entry.key"></q-item-label>
|
<q-item-label v-text="entry.key"></q-item-label>
|
||||||
<q-item-label caption v-text="entry.value"></q-item-label>
|
<q-item-label caption v-text="entry.value"></q-item-label>
|
||||||
@@ -880,14 +885,37 @@
|
|||||||
:props="props"
|
:props="props"
|
||||||
style="white-space: normal; word-break: break-all"
|
style="white-space: normal; word-break: break-all"
|
||||||
>
|
>
|
||||||
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
<q-icon
|
||||||
|
v-if="
|
||||||
|
props.row.isIn &&
|
||||||
|
props.row.isPending &&
|
||||||
|
props.row.extra.hold_invoice
|
||||||
|
"
|
||||||
|
name="pause_presentation"
|
||||||
|
color="grey"
|
||||||
|
class="cursor-pointer q-mr-sm"
|
||||||
|
@click="showHoldInvoiceDialog(props.row)"
|
||||||
|
>
|
||||||
|
<q-tooltip><span v-text="$t('hold_invoice')"></span></q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
<q-badge
|
||||||
|
v-if="props.row.tag"
|
||||||
|
color="yellow"
|
||||||
|
text-color="black"
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
v-text="'#' + props.row.tag"
|
v-text="'#' + props.row.tag"
|
||||||
class="inherit"
|
class="inherit"
|
||||||
:href="['/', props.row.tag].join('')"
|
:href="['/', props.row.tag].join('')"
|
||||||
></a>
|
></a>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
<span class="q-ml-sm" v-text="props.row.memo"></span>
|
<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>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<i>
|
<i>
|
||||||
@@ -931,7 +959,7 @@
|
|||||||
</q-td>
|
</q-td>
|
||||||
<q-dialog v-model="props.expand" :props="props" position="top">
|
<q-dialog v-model="props.expand" :props="props" position="top">
|
||||||
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
|
||||||
<q-card-section class="">
|
<q-card-section>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
expand-separator
|
expand-separator
|
||||||
@@ -994,6 +1022,7 @@
|
|||||||
></lnbits-payment-details>
|
></lnbits-payment-details>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
|
||||||
class="text-center q-my-lg"
|
class="text-center q-my-lg"
|
||||||
@@ -1050,6 +1079,55 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
<q-dialog v-model="hodlInvoice.show" position="top">
|
||||||
|
<q-card class="q-pa-sm q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-card-section>
|
||||||
|
<q-item-label class="text-h6">
|
||||||
|
<span v-text="$t('hold_invoice')"></span>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label class="text-subtitle2">
|
||||||
|
<span v-text="$t('hold_invoice_description')"></span>
|
||||||
|
</q-item-label>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
:label="$t('preimage')"
|
||||||
|
:hint="$t('preimage_hint')"
|
||||||
|
v-model="hodlInvoice.preimage"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="settleHoldInvoice(hodlInvoice.preimage)"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="row q-gutter-x-sm">
|
||||||
|
<q-btn
|
||||||
|
@click="settleHoldInvoice(hodlInvoice.preimage)"
|
||||||
|
outline
|
||||||
|
v-close-popup
|
||||||
|
color="grey"
|
||||||
|
:label="$t('settle_invoice')"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-sm"
|
||||||
|
@click="cancelHoldInvoice(hodlInvoice.payment.payment_hash)"
|
||||||
|
:label="$t('cancel_invoice')"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
class="q-ml-auto"
|
||||||
|
:label="$t('close')"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class AESCipher:
|
|||||||
self, salt: bytes, output_len: int = 32 + 16
|
self, salt: bytes, output_len: int = 32 + 16
|
||||||
) -> tuple[bytes, bytes]:
|
) -> tuple[bytes, bytes]:
|
||||||
# extended from https://gist.github.com/gsakkis/4546068
|
# extended from https://gist.github.com/gsakkis/4546068
|
||||||
assert len(salt) == 8, "Salt must be 8 bytes"
|
if len(salt) != 8:
|
||||||
|
raise ValueError("Salt must be 8 bytes")
|
||||||
data = self.key + salt
|
data = self.key + salt
|
||||||
key = md5(data).digest()
|
key = md5(data).digest()
|
||||||
final_key = key
|
final_key = key
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
def log_server_info():
|
def log_server_info():
|
||||||
logger.info("Starting LNbits")
|
logger.info("LNbits Info")
|
||||||
logger.info(f"Version: {settings.version}")
|
logger.info(f"Version: {settings.version}")
|
||||||
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
||||||
logger.info(f"Host: {settings.host}")
|
logger.info(f"Host: {settings.host}")
|
||||||
|
|||||||
@@ -175,14 +175,17 @@ def normalize_private_key(key: str) -> str:
|
|||||||
def normalize_bech32_key(hrp: str, key: str) -> str:
|
def normalize_bech32_key(hrp: str, key: str) -> str:
|
||||||
if key.startswith(hrp):
|
if key.startswith(hrp):
|
||||||
_, decoded_data = bech32_decode(key)
|
_, decoded_data = bech32_decode(key)
|
||||||
assert decoded_data, f"Key is not valid {hrp}."
|
if not decoded_data:
|
||||||
|
raise ValueError(f"Key is not valid {hrp}.")
|
||||||
|
|
||||||
decoded_data_bits = convertbits(decoded_data, 5, 8, False)
|
decoded_data_bits = convertbits(decoded_data, 5, 8, False)
|
||||||
assert decoded_data_bits, f"Key is not valid {hrp}."
|
if not decoded_data_bits:
|
||||||
|
raise ValueError(f"Key is not valid {hrp}.")
|
||||||
|
|
||||||
return bytes(decoded_data_bits).hex()
|
return bytes(decoded_data_bits).hex()
|
||||||
|
|
||||||
assert len(key) == 64, "Key has wrong length."
|
if len(key) != 64:
|
||||||
|
raise ValueError("Key has wrong length.")
|
||||||
try:
|
try:
|
||||||
int(key, 16)
|
int(key, 16)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -203,7 +206,8 @@ def hex_to_npub(hex_pubkey: str) -> str:
|
|||||||
normalize_public_key(hex_pubkey)
|
normalize_public_key(hex_pubkey)
|
||||||
pubkey_bytes = bytes.fromhex(hex_pubkey)
|
pubkey_bytes = bytes.fromhex(hex_pubkey)
|
||||||
bits = convertbits(pubkey_bytes, 8, 5, True)
|
bits = convertbits(pubkey_bytes, 8, 5, True)
|
||||||
assert bits
|
if not bits:
|
||||||
|
raise ValueError("Invalid Nostr public key.")
|
||||||
return bech32_encode("npub", bits)
|
return bech32_encode("npub", bits)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
MIIBYzCCARWgAwIBAgIHPjlTc1IbAzAFBgMrZXAwEDEOMAwGA1UEAxMFQnJlZXowHhcNMjUwNzAyMDkxODMzWhcNMzUwNjMwMDkxODMzWjAfMQ8wDQYDVQQKEwZsbmJpdHMxDDAKBgNVBAMTA2RuaTAqMAUGAytlcAMhANCD9cvfIDwcoiDKKYdT9BunHLS2/OuKzV8NS0SzqV13o38wfTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2jmj7l5rSw0yVb/vlWAYkK/YBwkwHwYDVR0jBBgwFoAU3qrWklbzjed0khb8TLYgsmsomGswHQYDVR0RBBYwFIESb2ZmaWNlQGRuaWxhYnMuY29tMAUGAytlcANBAMGS8jEfZbfNpv6mVrg328NXnjA/nG6TuGA0aAw0NyDz499aeu/0TURjF8FzmxzmdNOiffUZ6akPWCZUKFYuGgA=
|
||||||
@@ -2,15 +2,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
from lnbits.nodes import set_node_class
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.wallets.base import Wallet
|
from lnbits.wallets.base import Feature, Wallet
|
||||||
|
|
||||||
from .alby import AlbyWallet
|
from .alby import AlbyWallet
|
||||||
from .blink import BlinkWallet
|
from .blink import BlinkWallet
|
||||||
from .boltz import BoltzWallet
|
from .boltz import BoltzWallet
|
||||||
from .breez import BreezSdkWallet
|
from .breez import BreezSdkWallet
|
||||||
|
from .breez_liquid import BreezLiquidSdkWallet
|
||||||
from .cliche import ClicheWallet
|
from .cliche import ClicheWallet
|
||||||
|
from .clnrest import CLNRestWallet
|
||||||
from .corelightning import CoreLightningWallet
|
from .corelightning import CoreLightningWallet
|
||||||
|
|
||||||
# The following import is intentional to keep backwards compatibility
|
# The following import is intentional to keep backwards compatibility
|
||||||
@@ -33,13 +34,13 @@ from .void import VoidWallet
|
|||||||
from .zbd import ZBDWallet
|
from .zbd import ZBDWallet
|
||||||
|
|
||||||
|
|
||||||
def set_funding_source(class_name: str | None = None):
|
def set_funding_source(class_name: str | None = None) -> None:
|
||||||
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
||||||
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
||||||
global funding_source
|
global funding_source
|
||||||
funding_source = funding_source_constructor()
|
funding_source = funding_source_constructor()
|
||||||
if funding_source.__node_cls__:
|
settings.has_nodemanager = funding_source.has_feature(Feature.nodemanager)
|
||||||
set_node_class(funding_source.__node_cls__(funding_source))
|
settings.has_holdinvoice = funding_source.has_feature(Feature.holdinvoice)
|
||||||
|
|
||||||
|
|
||||||
def get_funding_source() -> Wallet:
|
def get_funding_source() -> Wallet:
|
||||||
@@ -57,7 +58,9 @@ __all__ = [
|
|||||||
"AlbyWallet",
|
"AlbyWallet",
|
||||||
"BlinkWallet",
|
"BlinkWallet",
|
||||||
"BoltzWallet",
|
"BoltzWallet",
|
||||||
|
"BreezLiquidSdkWallet",
|
||||||
"BreezSdkWallet",
|
"BreezSdkWallet",
|
||||||
|
"CLNRestWallet",
|
||||||
"CLightningWallet",
|
"CLightningWallet",
|
||||||
"ClicheWallet",
|
"ClicheWallet",
|
||||||
"CoreLightningRestWallet",
|
"CoreLightningRestWallet",
|
||||||
|
|||||||
+39
-1
@@ -3,14 +3,24 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import AsyncGenerator, Coroutine
|
from collections.abc import AsyncGenerator, Coroutine
|
||||||
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, NamedTuple
|
from typing import TYPE_CHECKING, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.exceptions import InvoiceError
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lnbits.nodes.base import Node
|
from lnbits.nodes.base import Node
|
||||||
|
|
||||||
|
|
||||||
|
class Feature(Enum):
|
||||||
|
nodemanager = "nodemanager"
|
||||||
|
holdinvoice = "holdinvoice"
|
||||||
|
# bolt12 = "bolt12"
|
||||||
|
|
||||||
|
|
||||||
class StatusResponse(NamedTuple):
|
class StatusResponse(NamedTuple):
|
||||||
error_message: str | None
|
error_message: str | None
|
||||||
balance_msat: int
|
balance_msat: int
|
||||||
@@ -22,6 +32,7 @@ class InvoiceResponse(NamedTuple):
|
|||||||
payment_request: str | None = None
|
payment_request: str | None = None
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
preimage: str | None = None
|
preimage: str | None = None
|
||||||
|
fee_msat: int | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -97,6 +108,10 @@ class PaymentPendingStatus(PaymentStatus):
|
|||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|
||||||
__node_cls__: type[Node] | None = None
|
__node_cls__: type[Node] | None = None
|
||||||
|
features: list[Feature] | None = None
|
||||||
|
|
||||||
|
def has_feature(self, feature: Feature) -> bool:
|
||||||
|
return self.features is not None and feature in self.features
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.pending_invoices: list[str] = []
|
self.pending_invoices: list[str] = []
|
||||||
@@ -138,8 +153,31 @@ class Wallet(ABC):
|
|||||||
) -> Coroutine[None, None, PaymentStatus]:
|
) -> Coroutine[None, None, PaymentStatus]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def create_hold_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
payment_hash: str,
|
||||||
|
memo: str | None = None,
|
||||||
|
description_hash: bytes | None = None,
|
||||||
|
unhashed_description: bytes | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
raise InvoiceError(
|
||||||
|
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||||
|
raise InvoiceError(
|
||||||
|
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||||
|
raise InvoiceError(
|
||||||
|
message="Hold invoices are not supported by this wallet.", status="failed"
|
||||||
|
)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while settings.lnbits_running:
|
||||||
for invoice in self.pending_invoices:
|
for invoice in self.pending_invoices:
|
||||||
try:
|
try:
|
||||||
status = await self.get_invoice_status(invoice)
|
status = await self.get_invoice_status(invoice)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ from typing import Optional
|
|||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from websockets.legacy.client import WebSocketClientProtocol, connect
|
from websockets import Subprotocol, connect
|
||||||
from websockets.typing import Subprotocol
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
@@ -48,7 +47,7 @@ class BlinkWallet(Wallet):
|
|||||||
"payload": {"X-API-KEY": settings.blink_token},
|
"payload": {"X-API-KEY": settings.blink_token},
|
||||||
}
|
}
|
||||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||||
self.ws: Optional[WebSocketClientProtocol] = None
|
self.ws = None
|
||||||
self._wallet_id = None
|
self._wallet_id = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -252,7 +251,8 @@ class BlinkWallet(Wallet):
|
|||||||
response = await self._graphql_query(data)
|
response = await self._graphql_query(data)
|
||||||
|
|
||||||
response_data = response.get("data")
|
response_data = response.get("data")
|
||||||
assert response_data is not None
|
if response_data is None:
|
||||||
|
raise ValueError("No data found in response.")
|
||||||
txs_data = (
|
txs_data = (
|
||||||
response_data.get("me", {})
|
response_data.get("me", {})
|
||||||
.get("defaultAccount", {})
|
.get("defaultAccount", {})
|
||||||
@@ -260,7 +260,8 @@ class BlinkWallet(Wallet):
|
|||||||
.get("transactionsByPaymentHash", [])
|
.get("transactionsByPaymentHash", [])
|
||||||
)
|
)
|
||||||
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
|
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
|
||||||
assert tx_data, "No SEND data found."
|
if not tx_data:
|
||||||
|
raise ValueError("No SEND data found.")
|
||||||
fee = tx_data.get("settlementFee")
|
fee = tx_data.get("settlementFee")
|
||||||
preimage = tx_data.get("settlementVia", {}).get("preImage")
|
preimage = tx_data.get("settlementVia", {}).get("preImage")
|
||||||
status = tx_data.get("status")
|
status = tx_data.get("status")
|
||||||
@@ -284,9 +285,8 @@ class BlinkWallet(Wallet):
|
|||||||
await ws.send(json.dumps(self.ws_auth))
|
await ws.send(json.dumps(self.ws_auth))
|
||||||
confirmation = await ws.recv()
|
confirmation = await ws.recv()
|
||||||
ack = json.loads(confirmation)
|
ack = json.loads(confirmation)
|
||||||
assert (
|
if ack.get("type") != "connection_ack":
|
||||||
ack.get("type") == "connection_ack"
|
raise ValueError("Websocket connection not acknowledged.")
|
||||||
), "Websocket connection not acknowledged."
|
|
||||||
|
|
||||||
logger.info("Websocket connection acknowledged.")
|
logger.info("Websocket connection acknowledged.")
|
||||||
subscription_req = {
|
subscription_req = {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class BoltzWallet(Wallet):
|
|||||||
channel = grpc.aio.insecure_channel(settings.boltz_client_endpoint)
|
channel = grpc.aio.insecure_channel(settings.boltz_client_endpoint)
|
||||||
|
|
||||||
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
|
self.rpc = boltzrpc_pb2_grpc.BoltzStub(channel)
|
||||||
self.wallet_id: int = 0
|
self.wallet_id = 0
|
||||||
|
|
||||||
# Auto-create wallet if running in Docker mode
|
# Auto-create wallet if running in Docker mode
|
||||||
async def _init_boltz_wallet():
|
async def _init_boltz_wallet():
|
||||||
@@ -142,7 +142,8 @@ class BoltzWallet(Wallet):
|
|||||||
pair_info = await self.rpc.GetPairInfo(pair_request, metadata=self.metadata)
|
pair_info = await self.rpc.GetPairInfo(pair_request, metadata=self.metadata)
|
||||||
invoice = decode(bolt11)
|
invoice = decode(bolt11)
|
||||||
|
|
||||||
assert invoice.amount_msat, "amountless invoice"
|
if not invoice.amount_msat:
|
||||||
|
raise ValueError("amountless invoice")
|
||||||
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
|
service_fee: float = invoice.amount_msat * pair_info.fees.percentage / 100
|
||||||
estimate = service_fee + pair_info.fees.miner_fees * 1000
|
estimate = service_fee + pair_info.fees.miner_fees * 1000
|
||||||
if estimate > fee_limit_msat:
|
if estimate > fee_limit_msat:
|
||||||
@@ -236,7 +237,7 @@ class BoltzWallet(Wallet):
|
|||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while settings.lnbits_running:
|
||||||
try:
|
try:
|
||||||
request = boltzrpc_pb2.GetSwapInfoRequest()
|
request = boltzrpc_pb2.GetSwapInfoRequest()
|
||||||
info: boltzrpc_pb2.GetSwapInfoResponse
|
info: boltzrpc_pb2.GetSwapInfoResponse
|
||||||
|
|||||||
+100
-74
@@ -1,15 +1,9 @@
|
|||||||
import base64
|
import base64
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
from lnbits.exceptions import UnsupportedError
|
from lnbits.exceptions import UnsupportedError
|
||||||
|
|
||||||
try:
|
if not find_spec("breez_sdk"):
|
||||||
import breez_sdk # type: ignore
|
|
||||||
|
|
||||||
BREEZ_SDK_INSTALLED = True
|
|
||||||
except ImportError:
|
|
||||||
BREEZ_SDK_INSTALLED = False
|
|
||||||
|
|
||||||
if not BREEZ_SDK_INSTALLED:
|
|
||||||
|
|
||||||
class BreezSdkWallet: # pyright: ignore
|
class BreezSdkWallet: # pyright: ignore
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -24,9 +18,31 @@ else:
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
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 loguru import logger
|
||||||
|
|
||||||
from lnbits import bolt11 as lnbits_bolt11
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -40,7 +56,15 @@ else:
|
|||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
breez_event_queue: asyncio.Queue = asyncio.Queue()
|
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)
|
||||||
|
|
||||||
def load_bytes(source: str, extension: str) -> Optional[bytes]:
|
def load_bytes(source: str, extension: str) -> Optional[bytes]:
|
||||||
# first check if it can be read from a file
|
# first check if it can be read from a file
|
||||||
@@ -57,15 +81,11 @@ else:
|
|||||||
# else convert from base64
|
# else convert from base64
|
||||||
try:
|
try:
|
||||||
return base64.b64decode(source)
|
return base64.b64decode(source)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logger.debug(exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_greenlight_credentials() -> (
|
def load_greenlight_credentials() -> Optional[GreenlightCredentials]:
|
||||||
Optional[
|
|
||||||
breez_sdk.GreenlightCredentials # pyright: ignore[reportUnboundVariable]
|
|
||||||
]
|
|
||||||
):
|
|
||||||
if (
|
if (
|
||||||
settings.breez_greenlight_device_key
|
settings.breez_greenlight_device_key
|
||||||
and settings.breez_greenlight_device_cert
|
and settings.breez_greenlight_device_cert
|
||||||
@@ -78,19 +98,12 @@ else:
|
|||||||
"cannot decode breez_greenlight_device_key "
|
"cannot decode breez_greenlight_device_key "
|
||||||
"or breez_greenlight_device_cert"
|
"or breez_greenlight_device_cert"
|
||||||
)
|
)
|
||||||
return breez_sdk.GreenlightCredentials( # pyright: ignore[reportUnboundVariable]
|
return GreenlightCredentials(
|
||||||
developer_key=list(device_key_bytes),
|
developer_key=list(device_key_bytes),
|
||||||
developer_cert=list(device_cert_bytes),
|
developer_cert=list(device_cert_bytes),
|
||||||
)
|
)
|
||||||
return None
|
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]
|
class BreezSdkWallet(Wallet): # type: ignore[no-redef]
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not settings.breez_greenlight_seed:
|
if not settings.breez_greenlight_seed:
|
||||||
@@ -118,15 +131,15 @@ else:
|
|||||||
"missing breez_greenlight_device_key"
|
"missing breez_greenlight_device_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.config = breez_sdk.default_config(
|
gl_config = GreenlightNodeConfig(
|
||||||
breez_sdk.EnvironmentType.PRODUCTION,
|
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,
|
||||||
settings.breez_api_key,
|
settings.breez_api_key,
|
||||||
breez_sdk.NodeConfig.GREENLIGHT(
|
node_config=node_config, # type: ignore[arg-type]
|
||||||
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")
|
breez_sdk_working_dir = Path(settings.lnbits_data_folder, "breez-sdk")
|
||||||
@@ -134,9 +147,9 @@ else:
|
|||||||
self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
|
self.config.working_dir = breez_sdk_working_dir.absolute().as_posix()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
seed = breez_sdk.mnemonic_to_seed(settings.breez_greenlight_seed)
|
seed = mnemonic_to_seed(settings.breez_greenlight_seed)
|
||||||
connect_request = breez_sdk.ConnectRequest(self.config, seed)
|
connect_request = ConnectRequest(config=self.config, seed=seed)
|
||||||
self.sdk_services = breez_sdk.connect(connect_request, SDKListener())
|
self.sdk_services = connect(connect_request, PaymentsListener())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
raise ValueError(f"cannot initialize BreezSdkWallet: {exc!s}") from exc
|
raise ValueError(f"cannot initialize BreezSdkWallet: {exc!s}") from exc
|
||||||
@@ -146,7 +159,7 @@ else:
|
|||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
node_info: breez_sdk.NodeState = self.sdk_services.node_info()
|
node_info = self.sdk_services.node_info()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
|
return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0)
|
||||||
|
|
||||||
@@ -168,14 +181,14 @@ else:
|
|||||||
"'description_hash' unsupported by Greenlight, provide"
|
"'description_hash' unsupported by Greenlight, provide"
|
||||||
" 'unhashed_description'"
|
" 'unhashed_description'"
|
||||||
)
|
)
|
||||||
breez_invoice: breez_sdk.ReceivePaymentResponse = (
|
breez_invoice: ReceivePaymentResponse = (
|
||||||
self.sdk_services.receive_payment(
|
self.sdk_services.receive_payment(
|
||||||
breez_sdk.ReceivePaymentRequest(
|
ReceivePaymentRequest(
|
||||||
amount * 1000, # breez uses msat
|
amount_msat=amount * 1000, # breez uses msat
|
||||||
(
|
description=(
|
||||||
unhashed_description.decode()
|
unhashed_description.decode()
|
||||||
if unhashed_description
|
if unhashed_description
|
||||||
else memo
|
else memo or ""
|
||||||
),
|
),
|
||||||
preimage=kwargs.get("preimage"),
|
preimage=kwargs.get("preimage"),
|
||||||
opening_fee_params=None,
|
opening_fee_params=None,
|
||||||
@@ -198,36 +211,45 @@ else:
|
|||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
self, bolt11: str, fee_limit_msat: int
|
self, bolt11: str, fee_limit_msat: int
|
||||||
) -> PaymentResponse:
|
) -> PaymentResponse:
|
||||||
invoice = lnbits_bolt11.decode(bolt11)
|
logger.debug(f"fee_limit_msat {fee_limit_msat} is ignored by Breez SDK")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_payment_request = breez_sdk.SendPaymentRequest(
|
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(
|
||||||
bolt11=bolt11, use_trampoline=settings.breez_use_trampoline
|
bolt11=bolt11, use_trampoline=settings.breez_use_trampoline
|
||||||
)
|
)
|
||||||
send_payment_response: breez_sdk.SendPaymentResponse = (
|
send_payment_response: SendPaymentResponse = (
|
||||||
self.sdk_services.send_payment(send_payment_request)
|
self.sdk_services.send_payment(send_payment_request)
|
||||||
)
|
)
|
||||||
payment: breez_sdk.Payment = send_payment_response.payment
|
payment = send_payment_response.payment
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
try:
|
try:
|
||||||
# try to report issue to Breez to improve LSP routing
|
# report issue to Breez to improve LSP routing
|
||||||
self.sdk_services.report_issue(
|
payment_error = ReportIssueRequest.PAYMENT_FAILURE(
|
||||||
breez_sdk.ReportIssueRequest.PAYMENT_FAILURE(
|
ReportPaymentFailureDetails(payment_hash=invoice.payment_hash)
|
||||||
breez_sdk.ReportPaymentFailureDetails(invoice.payment_hash)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
self.sdk_services.report_issue(payment_error) # type: ignore[arg-type]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.info(ex)
|
logger.info(ex)
|
||||||
# assume that payment failed?
|
return PaymentResponse(error_message=f"exception while payment {exc!s}")
|
||||||
return PaymentResponse(ok=False, error_message=f"payment failed: {exc}")
|
|
||||||
|
|
||||||
if payment.status != breez_sdk.PaymentStatus.COMPLETE:
|
if payment.status != BreezPaymentStatus.COMPLETE:
|
||||||
return PaymentResponse(ok=False, error_message="payment is pending")
|
return PaymentResponse(ok=None, error_message="payment is pending")
|
||||||
|
|
||||||
# let's use the payment_hash as the checking_id
|
# let's use the payment_hash as the checking_id
|
||||||
checking_id = invoice.payment_hash
|
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(
|
return PaymentResponse(
|
||||||
ok=True,
|
ok=True,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
@@ -237,18 +259,22 @@ else:
|
|||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
|
payment = self.sdk_services.payment_by_hash(checking_id)
|
||||||
checking_id
|
|
||||||
)
|
|
||||||
if payment is None:
|
if payment is None:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
if payment.payment_type != breez_sdk.PaymentType.RECEIVED:
|
if payment.payment_type != PaymentType.RECEIVED:
|
||||||
logger.warning(f"unexpected payment type: {payment.status}")
|
logger.warning(f"unexpected payment type: {payment.status}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
if payment.status == breez_sdk.PaymentStatus.FAILED:
|
if not isinstance(payment.details, PaymentDetails.LN):
|
||||||
|
logger.warning(f"unexpected paymentdetails type: {payment.details}")
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
if payment.status == BreezPaymentStatus.FAILED:
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
|
if payment.status == BreezPaymentStatus.COMPLETE:
|
||||||
return PaymentSuccessStatus()
|
return PaymentSuccessStatus(
|
||||||
|
fee_msat=payment.fee_msat,
|
||||||
|
preimage=payment.details.data.payment_preimage,
|
||||||
|
)
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
@@ -256,20 +282,21 @@ else:
|
|||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
try:
|
||||||
payment: breez_sdk.Payment = self.sdk_services.payment_by_hash(
|
payment = self.sdk_services.payment_by_hash(checking_id)
|
||||||
checking_id
|
|
||||||
)
|
|
||||||
if payment is None:
|
if payment is None:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
if payment.payment_type != breez_sdk.PaymentType.SENT:
|
if payment.payment_type != PaymentType.SENT:
|
||||||
logger.warning(f"unexpected payment type: {payment.status}")
|
logger.warning(f"unexpected payment type: {payment.payment_type}")
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
if payment.status == breez_sdk.PaymentStatus.COMPLETE:
|
if not isinstance(payment.details, PaymentDetails.LN):
|
||||||
|
logger.warning(f"unexpected paymentdetails type: {payment.details}")
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
if payment.status == BreezPaymentStatus.COMPLETE:
|
||||||
return PaymentSuccessStatus(
|
return PaymentSuccessStatus(
|
||||||
fee_msat=payment.fee_msat,
|
fee_msat=payment.fee_msat,
|
||||||
preimage=payment.details.data.payment_preimage,
|
preimage=payment.details.data.payment_preimage,
|
||||||
)
|
)
|
||||||
if payment.status == breez_sdk.PaymentStatus.FAILED:
|
if payment.status == BreezPaymentStatus.FAILED:
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -277,7 +304,6 @@ else:
|
|||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while True:
|
while settings.lnbits_running:
|
||||||
event = await breez_event_queue.get()
|
details = await breez_incoming_queue.get()
|
||||||
if event.is_invoice_paid():
|
yield details.data.payment_hash
|
||||||
yield event.details.payment_hash
|
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
# 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)
|
||||||
@@ -182,7 +182,8 @@ class ClicheWallet(Wallet):
|
|||||||
try:
|
try:
|
||||||
if data["result"]["status"]:
|
if data["result"]["status"]:
|
||||||
yield data["result"]["payment_hash"]
|
yield data["result"]["payment_hash"]
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
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}"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from secrets import token_urlsafe
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from bolt11.decode import decode as bolt11_decode
|
from bolt11.decode import decode as bolt11_decode
|
||||||
@@ -14,6 +14,7 @@ from lnbits.settings import settings
|
|||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
|
Feature,
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentFailedStatus,
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
@@ -31,12 +32,16 @@ async def run_sync(func) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
class CoreLightningWallet(Wallet):
|
class CoreLightningWallet(Wallet):
|
||||||
|
"""Core Lightning RPC implementation."""
|
||||||
|
|
||||||
__node_cls__ = CoreLightningNode
|
__node_cls__ = CoreLightningNode
|
||||||
|
features = [Feature.nodemanager]
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
rpc = settings.corelightning_rpc or settings.clightning_rpc
|
rpc = settings.corelightning_rpc or settings.clightning_rpc
|
||||||
if not rpc:
|
if not rpc:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -92,7 +97,7 @@ class CoreLightningWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = kwargs.get("label", f"lbl{random.random()}")
|
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
|
||||||
msat: int = int(amount * 1000)
|
msat: int = int(amount * 1000)
|
||||||
try:
|
try:
|
||||||
if description_hash and not unhashed_description:
|
if description_hash and not unhashed_description:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import random
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from secrets import token_urlsafe
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -111,7 +111,7 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = kwargs.get("label", f"lbl{random.random()}")
|
label = kwargs.get("label", f"lbl{token_urlsafe(16)}")
|
||||||
data: dict = {
|
data: dict = {
|
||||||
"amount": amount * 1000,
|
"amount": amount * 1000,
|
||||||
"description": memo,
|
"description": memo,
|
||||||
@@ -298,7 +298,8 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
self.last_pay_index = inv["pay_index"]
|
self.last_pay_index = inv["pay_index"]
|
||||||
if not paid:
|
if not paid:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
logger.trace(f"paid invoice: {inv}")
|
logger.trace(f"paid invoice: {inv}")
|
||||||
|
|
||||||
@@ -313,11 +314,12 @@ class CoreLightningRestWallet(Wallet):
|
|||||||
)
|
)
|
||||||
paid_invoice = r.json()
|
paid_invoice = r.json()
|
||||||
logger.trace(f"paid invoice: {paid_invoice}")
|
logger.trace(f"paid invoice: {paid_invoice}")
|
||||||
assert self.statuses[
|
if not self.statuses[paid_invoice["invoices"][0]["status"]]:
|
||||||
paid_invoice["invoices"][0]["status"]
|
raise ValueError("streamed invoice not paid")
|
||||||
], "streamed invoice not paid"
|
if "invoices" not in paid_invoice:
|
||||||
assert "invoices" in paid_invoice, "no invoices in response"
|
raise ValueError("no invoices in response")
|
||||||
assert len(paid_invoice["invoices"]), "no invoices in response"
|
if not len(paid_invoice["invoices"]):
|
||||||
|
raise ValueError("no invoices in response")
|
||||||
yield paid_invoice["invoices"][0]["payment_hash"]
|
yield paid_invoice["invoices"][0]["payment_hash"]
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.legacy.client import connect
|
from websockets import connect
|
||||||
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -245,7 +245,9 @@ class EclairWallet(Wallet):
|
|||||||
try:
|
try:
|
||||||
async with connect(
|
async with connect(
|
||||||
self.ws_url,
|
self.ws_url,
|
||||||
extra_headers=[("Authorization", self.headers["Authorization"])],
|
additional_headers=[
|
||||||
|
("Authorization", self.headers["Authorization"])
|
||||||
|
],
|
||||||
) as ws:
|
) as ws:
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
message = await ws.recv()
|
message = await ws.recv()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.legacy.client import connect
|
from websockets import connect
|
||||||
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||||
|
# NO CHECKED-IN PROTOBUF GENCODE
|
||||||
|
# source: invoices.proto
|
||||||
|
# Protobuf Python Version: 5.28.1
|
||||||
|
"""Generated protocol buffer code."""
|
||||||
|
from google.protobuf import descriptor as _descriptor
|
||||||
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
|
from google.protobuf import runtime_version as _runtime_version
|
||||||
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||||
|
_runtime_version.Domain.PUBLIC,
|
||||||
|
5,
|
||||||
|
28,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
'invoices.proto'
|
||||||
|
)
|
||||||
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
|
_sym_db = _symbol_database.Default()
|
||||||
|
|
||||||
|
|
||||||
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
|
||||||
|
|
||||||
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0einvoices.proto\x12\x0binvoicesrpc\x1a\x0flightning.proto\"(\n\x10\x43\x61ncelInvoiceMsg\x12\x14\n\x0cpayment_hash\x18\x01 \x01(\x0c\"\x13\n\x11\x43\x61ncelInvoiceResp\"\xe4\x01\n\x15\x41\x64\x64HoldInvoiceRequest\x12\x0c\n\x04memo\x18\x01 \x01(\t\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\x03\x12\x12\n\nvalue_msat\x18\n \x01(\x03\x12\x18\n\x10\x64\x65scription_hash\x18\x04 \x01(\x0c\x12\x0e\n\x06\x65xpiry\x18\x05 \x01(\x03\x12\x15\n\rfallback_addr\x18\x06 \x01(\t\x12\x13\n\x0b\x63ltv_expiry\x18\x07 \x01(\x04\x12%\n\x0broute_hints\x18\x08 \x03(\x0b\x32\x10.lnrpc.RouteHint\x12\x0f\n\x07private\x18\t \x01(\x08\"V\n\x12\x41\x64\x64HoldInvoiceResp\x12\x17\n\x0fpayment_request\x18\x01 \x01(\t\x12\x11\n\tadd_index\x18\x02 \x01(\x04\x12\x14\n\x0cpayment_addr\x18\x03 \x01(\x0c\"$\n\x10SettleInvoiceMsg\x12\x10\n\x08preimage\x18\x01 \x01(\x0c\"\x13\n\x11SettleInvoiceResp\"5\n\x1dSubscribeSingleInvoiceRequest\x12\x0e\n\x06r_hash\x18\x02 \x01(\x0cJ\x04\x08\x01\x10\x02\"\x99\x01\n\x10LookupInvoiceMsg\x12\x16\n\x0cpayment_hash\x18\x01 \x01(\x0cH\x00\x12\x16\n\x0cpayment_addr\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06set_id\x18\x03 \x01(\x0cH\x00\x12\x34\n\x0flookup_modifier\x18\x04 \x01(\x0e\x32\x1b.invoicesrpc.LookupModifierB\r\n\x0binvoice_ref\".\n\nCircuitKey\x12\x0f\n\x07\x63han_id\x18\x01 \x01(\x04\x12\x0f\n\x07htlc_id\x18\x02 \x01(\x04\"\xdd\x02\n\x11HtlcModifyRequest\x12\x1f\n\x07invoice\x18\x01 \x01(\x0b\x32\x0e.lnrpc.Invoice\x12\x36\n\x15\x65xit_htlc_circuit_key\x18\x02 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\rexit_htlc_amt\x18\x03 \x01(\x04\x12\x18\n\x10\x65xit_htlc_expiry\x18\x04 \x01(\r\x12\x16\n\x0e\x63urrent_height\x18\x05 \x01(\r\x12\x64\n\x1d\x65xit_htlc_wire_custom_records\x18\x06 \x03(\x0b\x32=.invoicesrpc.HtlcModifyRequest.ExitHtlcWireCustomRecordsEntry\x1a@\n\x1e\x45xitHtlcWireCustomRecordsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"z\n\x12HtlcModifyResponse\x12,\n\x0b\x63ircuit_key\x18\x01 \x01(\x0b\x32\x17.invoicesrpc.CircuitKey\x12\x15\n\x08\x61mt_paid\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x12\n\ncancel_set\x18\x03 \x01(\x08\x42\x0b\n\t_amt_paid*D\n\x0eLookupModifier\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x11\n\rHTLC_SET_ONLY\x10\x01\x12\x12\n\x0eHTLC_SET_BLANK\x10\x02\x32\xf0\x03\n\x08Invoices\x12V\n\x16SubscribeSingleInvoice\x12*.invoicesrpc.SubscribeSingleInvoiceRequest\x1a\x0e.lnrpc.Invoice0\x01\x12N\n\rCancelInvoice\x12\x1d.invoicesrpc.CancelInvoiceMsg\x1a\x1e.invoicesrpc.CancelInvoiceResp\x12U\n\x0e\x41\x64\x64HoldInvoice\x12\".invoicesrpc.AddHoldInvoiceRequest\x1a\x1f.invoicesrpc.AddHoldInvoiceResp\x12N\n\rSettleInvoice\x12\x1d.invoicesrpc.SettleInvoiceMsg\x1a\x1e.invoicesrpc.SettleInvoiceResp\x12@\n\x0fLookupInvoiceV2\x12\x1d.invoicesrpc.LookupInvoiceMsg\x1a\x0e.lnrpc.Invoice\x12S\n\x0cHtlcModifier\x12\x1f.invoicesrpc.HtlcModifyResponse\x1a\x1e.invoicesrpc.HtlcModifyRequest(\x01\x30\x01\x42\x33Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpcb\x06proto3')
|
||||||
|
|
||||||
|
_globals = globals()
|
||||||
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'invoices_pb2', _globals)
|
||||||
|
if not _descriptor._USE_C_DESCRIPTORS:
|
||||||
|
_globals['DESCRIPTOR']._loaded_options = None
|
||||||
|
_globals['DESCRIPTOR']._serialized_options = b'Z1github.com/lightningnetwork/lnd/lnrpc/invoicesrpc'
|
||||||
|
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._loaded_options = None
|
||||||
|
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_options = b'8\001'
|
||||||
|
_globals['_LOOKUPMODIFIER']._serialized_start=1224
|
||||||
|
_globals['_LOOKUPMODIFIER']._serialized_end=1292
|
||||||
|
_globals['_CANCELINVOICEMSG']._serialized_start=48
|
||||||
|
_globals['_CANCELINVOICEMSG']._serialized_end=88
|
||||||
|
_globals['_CANCELINVOICERESP']._serialized_start=90
|
||||||
|
_globals['_CANCELINVOICERESP']._serialized_end=109
|
||||||
|
_globals['_ADDHOLDINVOICEREQUEST']._serialized_start=112
|
||||||
|
_globals['_ADDHOLDINVOICEREQUEST']._serialized_end=340
|
||||||
|
_globals['_ADDHOLDINVOICERESP']._serialized_start=342
|
||||||
|
_globals['_ADDHOLDINVOICERESP']._serialized_end=428
|
||||||
|
_globals['_SETTLEINVOICEMSG']._serialized_start=430
|
||||||
|
_globals['_SETTLEINVOICEMSG']._serialized_end=466
|
||||||
|
_globals['_SETTLEINVOICERESP']._serialized_start=468
|
||||||
|
_globals['_SETTLEINVOICERESP']._serialized_end=487
|
||||||
|
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_start=489
|
||||||
|
_globals['_SUBSCRIBESINGLEINVOICEREQUEST']._serialized_end=542
|
||||||
|
_globals['_LOOKUPINVOICEMSG']._serialized_start=545
|
||||||
|
_globals['_LOOKUPINVOICEMSG']._serialized_end=698
|
||||||
|
_globals['_CIRCUITKEY']._serialized_start=700
|
||||||
|
_globals['_CIRCUITKEY']._serialized_end=746
|
||||||
|
_globals['_HTLCMODIFYREQUEST']._serialized_start=749
|
||||||
|
_globals['_HTLCMODIFYREQUEST']._serialized_end=1098
|
||||||
|
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_start=1034
|
||||||
|
_globals['_HTLCMODIFYREQUEST_EXITHTLCWIRECUSTOMRECORDSENTRY']._serialized_end=1098
|
||||||
|
_globals['_HTLCMODIFYRESPONSE']._serialized_start=1100
|
||||||
|
_globals['_HTLCMODIFYRESPONSE']._serialized_end=1222
|
||||||
|
_globals['_INVOICES']._serialized_start=1295
|
||||||
|
_globals['_INVOICES']._serialized_end=1791
|
||||||
|
# @@protoc_insertion_point(module_scope)
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||||
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
|
import grpc
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices__pb2
|
||||||
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as lightning__pb2
|
||||||
|
|
||||||
|
GRPC_GENERATED_VERSION = '1.68.1'
|
||||||
|
GRPC_VERSION = grpc.__version__
|
||||||
|
_version_not_supported = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from grpc._utilities import first_version_is_lower
|
||||||
|
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||||
|
except ImportError:
|
||||||
|
_version_not_supported = True
|
||||||
|
|
||||||
|
if _version_not_supported:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||||
|
+ f' but the generated code in invoices_pb2_grpc.py depends on'
|
||||||
|
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||||
|
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||||
|
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicesStub(object):
|
||||||
|
"""
|
||||||
|
Comments in this file will be directly parsed into the API
|
||||||
|
Documentation as descriptions of the associated method, message, or field.
|
||||||
|
These descriptions should go right above the definition of the object, and
|
||||||
|
can be in either block or // comment format.
|
||||||
|
|
||||||
|
An RPC method can be matched to an lncli command by placing a line in the
|
||||||
|
beginning of the description in exactly the following format:
|
||||||
|
lncli: `methodname`
|
||||||
|
|
||||||
|
Failure to specify the exact name of the command will cause documentation
|
||||||
|
generation to fail.
|
||||||
|
|
||||||
|
More information on how exactly the gRPC documentation is generated from
|
||||||
|
this proto file can be found here:
|
||||||
|
https://github.com/lightninglabs/lightning-api
|
||||||
|
|
||||||
|
Invoices is a service that can be used to create, accept, settle and cancel
|
||||||
|
invoices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: A grpc.Channel.
|
||||||
|
"""
|
||||||
|
self.SubscribeSingleInvoice = channel.unary_stream(
|
||||||
|
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
|
||||||
|
request_serializer=invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
|
||||||
|
response_deserializer=lightning__pb2.Invoice.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.CancelInvoice = channel.unary_unary(
|
||||||
|
'/invoicesrpc.Invoices/CancelInvoice',
|
||||||
|
request_serializer=invoices__pb2.CancelInvoiceMsg.SerializeToString,
|
||||||
|
response_deserializer=invoices__pb2.CancelInvoiceResp.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.AddHoldInvoice = channel.unary_unary(
|
||||||
|
'/invoicesrpc.Invoices/AddHoldInvoice',
|
||||||
|
request_serializer=invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
|
||||||
|
response_deserializer=invoices__pb2.AddHoldInvoiceResp.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.SettleInvoice = channel.unary_unary(
|
||||||
|
'/invoicesrpc.Invoices/SettleInvoice',
|
||||||
|
request_serializer=invoices__pb2.SettleInvoiceMsg.SerializeToString,
|
||||||
|
response_deserializer=invoices__pb2.SettleInvoiceResp.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.LookupInvoiceV2 = channel.unary_unary(
|
||||||
|
'/invoicesrpc.Invoices/LookupInvoiceV2',
|
||||||
|
request_serializer=invoices__pb2.LookupInvoiceMsg.SerializeToString,
|
||||||
|
response_deserializer=lightning__pb2.Invoice.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
self.HtlcModifier = channel.stream_stream(
|
||||||
|
'/invoicesrpc.Invoices/HtlcModifier',
|
||||||
|
request_serializer=invoices__pb2.HtlcModifyResponse.SerializeToString,
|
||||||
|
response_deserializer=invoices__pb2.HtlcModifyRequest.FromString,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicesServicer(object):
|
||||||
|
"""
|
||||||
|
Comments in this file will be directly parsed into the API
|
||||||
|
Documentation as descriptions of the associated method, message, or field.
|
||||||
|
These descriptions should go right above the definition of the object, and
|
||||||
|
can be in either block or // comment format.
|
||||||
|
|
||||||
|
An RPC method can be matched to an lncli command by placing a line in the
|
||||||
|
beginning of the description in exactly the following format:
|
||||||
|
lncli: `methodname`
|
||||||
|
|
||||||
|
Failure to specify the exact name of the command will cause documentation
|
||||||
|
generation to fail.
|
||||||
|
|
||||||
|
More information on how exactly the gRPC documentation is generated from
|
||||||
|
this proto file can be found here:
|
||||||
|
https://github.com/lightninglabs/lightning-api
|
||||||
|
|
||||||
|
Invoices is a service that can be used to create, accept, settle and cancel
|
||||||
|
invoices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def SubscribeSingleInvoice(self, request, context):
|
||||||
|
"""
|
||||||
|
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
|
||||||
|
to notify the client of state transitions of the specified invoice.
|
||||||
|
Initially the current invoice state is always sent out.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def CancelInvoice(self, request, context):
|
||||||
|
"""lncli: `cancelinvoice`
|
||||||
|
CancelInvoice cancels a currently open invoice. If the invoice is already
|
||||||
|
canceled, this call will succeed. If the invoice is already settled, it will
|
||||||
|
fail.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def AddHoldInvoice(self, request, context):
|
||||||
|
"""lncli: `addholdinvoice`
|
||||||
|
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||||
|
supplied in the request.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def SettleInvoice(self, request, context):
|
||||||
|
"""lncli: `settleinvoice`
|
||||||
|
SettleInvoice settles an accepted invoice. If the invoice is already
|
||||||
|
settled, this call will succeed.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def LookupInvoiceV2(self, request, context):
|
||||||
|
"""
|
||||||
|
LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced
|
||||||
|
using either its payment hash, payment address, or set ID.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
def HtlcModifier(self, request_iterator, context):
|
||||||
|
"""
|
||||||
|
HtlcModifier is a bidirectional streaming RPC that allows a client to
|
||||||
|
intercept and modify the HTLCs that attempt to settle the given invoice. The
|
||||||
|
server will send HTLCs of invoices to the client and the client can modify
|
||||||
|
some aspects of the HTLC in order to pass the invoice acceptance tests.
|
||||||
|
"""
|
||||||
|
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||||
|
context.set_details('Method not implemented!')
|
||||||
|
raise NotImplementedError('Method not implemented!')
|
||||||
|
|
||||||
|
|
||||||
|
def add_InvoicesServicer_to_server(servicer, server):
|
||||||
|
rpc_method_handlers = {
|
||||||
|
'SubscribeSingleInvoice': grpc.unary_stream_rpc_method_handler(
|
||||||
|
servicer.SubscribeSingleInvoice,
|
||||||
|
request_deserializer=invoices__pb2.SubscribeSingleInvoiceRequest.FromString,
|
||||||
|
response_serializer=lightning__pb2.Invoice.SerializeToString,
|
||||||
|
),
|
||||||
|
'CancelInvoice': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.CancelInvoice,
|
||||||
|
request_deserializer=invoices__pb2.CancelInvoiceMsg.FromString,
|
||||||
|
response_serializer=invoices__pb2.CancelInvoiceResp.SerializeToString,
|
||||||
|
),
|
||||||
|
'AddHoldInvoice': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.AddHoldInvoice,
|
||||||
|
request_deserializer=invoices__pb2.AddHoldInvoiceRequest.FromString,
|
||||||
|
response_serializer=invoices__pb2.AddHoldInvoiceResp.SerializeToString,
|
||||||
|
),
|
||||||
|
'SettleInvoice': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.SettleInvoice,
|
||||||
|
request_deserializer=invoices__pb2.SettleInvoiceMsg.FromString,
|
||||||
|
response_serializer=invoices__pb2.SettleInvoiceResp.SerializeToString,
|
||||||
|
),
|
||||||
|
'LookupInvoiceV2': grpc.unary_unary_rpc_method_handler(
|
||||||
|
servicer.LookupInvoiceV2,
|
||||||
|
request_deserializer=invoices__pb2.LookupInvoiceMsg.FromString,
|
||||||
|
response_serializer=lightning__pb2.Invoice.SerializeToString,
|
||||||
|
),
|
||||||
|
'HtlcModifier': grpc.stream_stream_rpc_method_handler(
|
||||||
|
servicer.HtlcModifier,
|
||||||
|
request_deserializer=invoices__pb2.HtlcModifyResponse.FromString,
|
||||||
|
response_serializer=invoices__pb2.HtlcModifyRequest.SerializeToString,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
|
'invoicesrpc.Invoices', rpc_method_handlers)
|
||||||
|
server.add_generic_rpc_handlers((generic_handler,))
|
||||||
|
server.add_registered_method_handlers('invoicesrpc.Invoices', rpc_method_handlers)
|
||||||
|
|
||||||
|
|
||||||
|
# This class is part of an EXPERIMENTAL API.
|
||||||
|
class Invoices(object):
|
||||||
|
"""
|
||||||
|
Comments in this file will be directly parsed into the API
|
||||||
|
Documentation as descriptions of the associated method, message, or field.
|
||||||
|
These descriptions should go right above the definition of the object, and
|
||||||
|
can be in either block or // comment format.
|
||||||
|
|
||||||
|
An RPC method can be matched to an lncli command by placing a line in the
|
||||||
|
beginning of the description in exactly the following format:
|
||||||
|
lncli: `methodname`
|
||||||
|
|
||||||
|
Failure to specify the exact name of the command will cause documentation
|
||||||
|
generation to fail.
|
||||||
|
|
||||||
|
More information on how exactly the gRPC documentation is generated from
|
||||||
|
this proto file can be found here:
|
||||||
|
https://github.com/lightninglabs/lightning-api
|
||||||
|
|
||||||
|
Invoices is a service that can be used to create, accept, settle and cancel
|
||||||
|
invoices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SubscribeSingleInvoice(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_stream(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/SubscribeSingleInvoice',
|
||||||
|
invoices__pb2.SubscribeSingleInvoiceRequest.SerializeToString,
|
||||||
|
lightning__pb2.Invoice.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def CancelInvoice(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/CancelInvoice',
|
||||||
|
invoices__pb2.CancelInvoiceMsg.SerializeToString,
|
||||||
|
invoices__pb2.CancelInvoiceResp.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def AddHoldInvoice(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/AddHoldInvoice',
|
||||||
|
invoices__pb2.AddHoldInvoiceRequest.SerializeToString,
|
||||||
|
invoices__pb2.AddHoldInvoiceResp.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SettleInvoice(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/SettleInvoice',
|
||||||
|
invoices__pb2.SettleInvoiceMsg.SerializeToString,
|
||||||
|
invoices__pb2.SettleInvoiceResp.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def LookupInvoiceV2(request,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.unary_unary(
|
||||||
|
request,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/LookupInvoiceV2',
|
||||||
|
invoices__pb2.LookupInvoiceMsg.SerializeToString,
|
||||||
|
lightning__pb2.Invoice.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def HtlcModifier(request_iterator,
|
||||||
|
target,
|
||||||
|
options=(),
|
||||||
|
channel_credentials=None,
|
||||||
|
call_credentials=None,
|
||||||
|
insecure=False,
|
||||||
|
compression=None,
|
||||||
|
wait_for_ready=None,
|
||||||
|
timeout=None,
|
||||||
|
metadata=None):
|
||||||
|
return grpc.experimental.stream_stream(
|
||||||
|
request_iterator,
|
||||||
|
target,
|
||||||
|
'/invoicesrpc.Invoices/HtlcModifier',
|
||||||
|
invoices__pb2.HtlcModifyResponse.SerializeToString,
|
||||||
|
invoices__pb2.HtlcModifyRequest.FromString,
|
||||||
|
options,
|
||||||
|
channel_credentials,
|
||||||
|
insecure,
|
||||||
|
call_credentials,
|
||||||
|
compression,
|
||||||
|
wait_for_ready,
|
||||||
|
timeout,
|
||||||
|
metadata,
|
||||||
|
_registered_method=True)
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,18 @@ from typing import Optional
|
|||||||
import grpc
|
import grpc
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
import lnbits.wallets.lnd_grpc_files.invoices_pb2 as invoices
|
||||||
|
import lnbits.wallets.lnd_grpc_files.invoices_pb2_grpc as invoicesrpc
|
||||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
||||||
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
||||||
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
||||||
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
from lnbits.wallets.lnd_grpc_files.router_pb2_grpc import RouterStub
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
|
Feature,
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentFailedStatus,
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
@@ -62,6 +65,8 @@ environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
|||||||
|
|
||||||
|
|
||||||
class LndWallet(Wallet):
|
class LndWallet(Wallet):
|
||||||
|
features = [Feature.holdinvoice]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not settings.lnd_grpc_endpoint:
|
if not settings.lnd_grpc_endpoint:
|
||||||
raise ValueError("cannot initialize LndWallet: missing lnd_grpc_endpoint")
|
raise ValueError("cannot initialize LndWallet: missing lnd_grpc_endpoint")
|
||||||
@@ -98,7 +103,8 @@ class LndWallet(Wallet):
|
|||||||
f"{self.endpoint}:{self.port}", composite_creds
|
f"{self.endpoint}:{self.port}", composite_creds
|
||||||
)
|
)
|
||||||
self.rpc = lnrpc.LightningStub(channel)
|
self.rpc = lnrpc.LightningStub(channel)
|
||||||
self.routerpc = routerrpc.RouterStub(channel)
|
self.routerpc = RouterStub(channel)
|
||||||
|
self.invoicesrpc = invoicesrpc.InvoicesStub(channel)
|
||||||
|
|
||||||
def metadata_callback(self, _, callback):
|
def metadata_callback(self, _, callback):
|
||||||
callback([("macaroon", self.macaroon)], None)
|
callback([("macaroon", self.macaroon)], None)
|
||||||
@@ -108,7 +114,7 @@ class LndWallet(Wallet):
|
|||||||
|
|
||||||
async def status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
|
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest()) # type: ignore
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
|
||||||
|
|
||||||
@@ -144,7 +150,7 @@ class LndWallet(Wallet):
|
|||||||
data["r_hash"] = bytes.fromhex(payment_hash)
|
data["r_hash"] = bytes.fromhex(payment_hash)
|
||||||
data["r_preimage"] = bytes.fromhex(preimage)
|
data["r_preimage"] = bytes.fromhex(preimage)
|
||||||
try:
|
try:
|
||||||
req = ln.Invoice(**data)
|
req = ln.Invoice(**data) # type: ignore
|
||||||
resp = await self.rpc.AddInvoice(req)
|
resp = await self.rpc.AddInvoice(req)
|
||||||
# response model
|
# response model
|
||||||
# {
|
# {
|
||||||
@@ -168,7 +174,7 @@ class LndWallet(Wallet):
|
|||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
||||||
req = router.SendPaymentRequest(
|
req = router.SendPaymentRequest( # type: ignore
|
||||||
payment_request=bolt11,
|
payment_request=bolt11,
|
||||||
fee_limit_msat=fee_limit_msat,
|
fee_limit_msat=fee_limit_msat,
|
||||||
timeout_seconds=30,
|
timeout_seconds=30,
|
||||||
@@ -227,7 +233,7 @@ class LndWallet(Wallet):
|
|||||||
# that use different checking_id formats
|
# that use different checking_id formats
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash)) # type: ignore
|
||||||
if resp.settled:
|
if resp.settled:
|
||||||
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
|
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
|
||||||
|
|
||||||
@@ -271,7 +277,7 @@ class LndWallet(Wallet):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp = self.routerpc.TrackPaymentV2(
|
resp = self.routerpc.TrackPaymentV2(
|
||||||
router.TrackPaymentRequest(payment_hash=r_hash)
|
router.TrackPaymentRequest(payment_hash=r_hash) # type: ignore
|
||||||
)
|
)
|
||||||
async for payment in resp:
|
async for payment in resp:
|
||||||
if len(payment.htlcs) and statuses[payment.status]:
|
if len(payment.htlcs) and statuses[payment.status]:
|
||||||
@@ -288,7 +294,7 @@ class LndWallet(Wallet):
|
|||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
try:
|
try:
|
||||||
request = ln.InvoiceSubscription()
|
request = ln.InvoiceSubscription() # type: ignore
|
||||||
async for i in self.rpc.SubscribeInvoices(request):
|
async for i in self.rpc.SubscribeInvoices(request):
|
||||||
if not i.settled:
|
if not i.settled:
|
||||||
continue
|
continue
|
||||||
@@ -301,3 +307,65 @@ class LndWallet(Wallet):
|
|||||||
"retrying in 5 seconds"
|
"retrying in 5 seconds"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def create_hold_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
payment_hash: str,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
data: dict = {
|
||||||
|
"description_hash": b"",
|
||||||
|
"value": amount,
|
||||||
|
"hash": hex_to_bytes(payment_hash),
|
||||||
|
"private": True,
|
||||||
|
"memo": memo or "",
|
||||||
|
}
|
||||||
|
if kwargs.get("expiry"):
|
||||||
|
data["expiry"] = kwargs["expiry"]
|
||||||
|
if description_hash:
|
||||||
|
data["description_hash"] = description_hash
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = sha256(unhashed_description).digest()
|
||||||
|
try:
|
||||||
|
req = invoices.AddHoldInvoiceRequest(**data) # type: ignore
|
||||||
|
res = await self.invoicesrpc.AddHoldInvoice(req)
|
||||||
|
logger.debug(f"AddHoldInvoice response: {res}")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
error_message = str(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=error_message)
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True, checking_id=payment_hash, payment_request=str(res.payment_request)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||||
|
try:
|
||||||
|
req = invoices.SettleInvoiceMsg(preimage=hex_to_bytes(preimage)) # type: ignore
|
||||||
|
await self.invoicesrpc.SettleInvoice(req)
|
||||||
|
except grpc.aio.AioRpcError as exc:
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, error_message=exc.details() or "unknown grpc exception"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
return InvoiceResponse(ok=True, preimage=preimage)
|
||||||
|
|
||||||
|
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||||
|
try:
|
||||||
|
req = invoices.CancelInvoiceMsg(payment_hash=hex_to_bytes(payment_hash)) # type: ignore
|
||||||
|
res = await self.invoicesrpc.CancelInvoice(req)
|
||||||
|
logger.debug(f"CancelInvoice response: {res}")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
# If we cannot cancel the invoice, we return an error message
|
||||||
|
# and True for ok that should be ignored by the service
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=False, checking_id=payment_hash, error_message=str(exc)
|
||||||
|
)
|
||||||
|
# If we reach here, the invoice was successfully canceled and payment failed
|
||||||
|
return InvoiceResponse(True, checking_id=payment_hash)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from lnbits.settings import settings
|
|||||||
from lnbits.utils.crypto import random_secret_and_hash
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
|
Feature,
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentFailedStatus,
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
@@ -27,9 +28,10 @@ from .macaroon import load_macaroon
|
|||||||
|
|
||||||
|
|
||||||
class LndRestWallet(Wallet):
|
class LndRestWallet(Wallet):
|
||||||
"""https://api.lightning.community/rest/index.html#lnd-rest-api-reference"""
|
"""https://api.lightning.community/#lnd-rest-api-reference"""
|
||||||
|
|
||||||
__node_cls__ = LndRestNode
|
__node_cls__ = LndRestNode
|
||||||
|
features = [Feature.nodemanager, Feature.holdinvoice]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not settings.lnd_rest_endpoint:
|
if not settings.lnd_rest_endpoint:
|
||||||
@@ -292,7 +294,8 @@ class LndRestWallet(Wallet):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
@@ -307,7 +310,8 @@ class LndRestWallet(Wallet):
|
|||||||
inv = json.loads(line)["result"]
|
inv = json.loads(line)["result"]
|
||||||
if not inv["settled"]:
|
if not inv["settled"]:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||||
@@ -318,3 +322,77 @@ class LndRestWallet(Wallet):
|
|||||||
" seconds"
|
" seconds"
|
||||||
)
|
)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def create_hold_invoice(
|
||||||
|
self,
|
||||||
|
amount: int,
|
||||||
|
payment_hash: str,
|
||||||
|
memo: Optional[str] = None,
|
||||||
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**_,
|
||||||
|
) -> InvoiceResponse:
|
||||||
|
data: dict = {
|
||||||
|
"value": amount,
|
||||||
|
"private": True,
|
||||||
|
"hash": base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii"),
|
||||||
|
}
|
||||||
|
if description_hash:
|
||||||
|
data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||||
|
"ascii"
|
||||||
|
)
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = base64.b64encode(
|
||||||
|
hashlib.sha256(unhashed_description).digest()
|
||||||
|
).decode("ascii")
|
||||||
|
else:
|
||||||
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = await self.client.post(url="/v2/invoices/hodl", json=data)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
|
payment_request = data["payment_request"]
|
||||||
|
payment_hash = base64.b64encode(bytes.fromhex(payment_hash)).decode("ascii")
|
||||||
|
|
||||||
|
return InvoiceResponse(
|
||||||
|
ok=True, checking_id=payment_hash, payment_request=payment_request
|
||||||
|
)
|
||||||
|
|
||||||
|
async def settle_hold_invoice(self, preimage: str) -> InvoiceResponse:
|
||||||
|
data: dict = {
|
||||||
|
"preimage": base64.b64encode(bytes.fromhex(preimage)).decode("ascii")
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
r = await self.client.post(url="/v2/invoices/settle", json=data)
|
||||||
|
r.raise_for_status()
|
||||||
|
return InvoiceResponse(ok=True, preimage=preimage)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
|
||||||
|
async def cancel_hold_invoice(self, payment_hash: str) -> InvoiceResponse:
|
||||||
|
rhash = bytes.fromhex(payment_hash)
|
||||||
|
try:
|
||||||
|
r = await self.client.post(
|
||||||
|
url="/v2/invoices/cancel",
|
||||||
|
json={"payment_hash": base64.b64encode(rhash).decode("ascii")},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
logger.debug(f"Cancel hold invoice response: {r.text}")
|
||||||
|
return InvoiceResponse(ok=True, checking_id=payment_hash)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
return InvoiceResponse(ok=False, error_message=exc.response.text)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return InvoiceResponse(ok=False, error_message=str(exc))
|
||||||
|
|||||||
@@ -173,11 +173,12 @@ class LnTipsWallet(Wallet):
|
|||||||
inv = json.loads(data)
|
inv = json.loads(data)
|
||||||
if not inv.get("payment_hash"):
|
if not inv.get("payment_hash"):
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
logger.debug(exc)
|
||||||
continue
|
continue
|
||||||
yield inv["payment_hash"]
|
yield inv["payment_hash"]
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logger.debug(exc)
|
||||||
|
|
||||||
# do not sleep if the connection was active for more than 10s
|
# do not sleep if the connection was active for more than 10s
|
||||||
# since the backend is expected to drop the connection after 90s
|
# since the backend is expected to drop the connection after 90s
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import base64
|
|||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +22,8 @@ def load_macaroon(
|
|||||||
aes = AESCipher(key.encode())
|
aes = AESCipher(key.encode())
|
||||||
return aes.decrypt(encrypted_macaroon)
|
return aes.decrypt(encrypted_macaroon)
|
||||||
|
|
||||||
assert macaroon, "macaroon must be set here"
|
if not macaroon:
|
||||||
|
raise ValueError("macaroon must be set here")
|
||||||
|
|
||||||
# if the macaroon is a file path, load it and return hex version
|
# if the macaroon is a file path, load it and return hex version
|
||||||
if macaroon.split(".")[-1] == "macaroon":
|
if macaroon.split(".")[-1] == "macaroon":
|
||||||
@@ -39,7 +42,7 @@ def load_macaroon(
|
|||||||
try:
|
try:
|
||||||
macaroon = base64.b64decode(macaroon).hex()
|
macaroon = base64.b64decode(macaroon).hex()
|
||||||
return macaroon
|
return macaroon
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logger.debug(exc)
|
||||||
|
|
||||||
return macaroon
|
return macaroon
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from urllib.parse import parse_qs, unquote, urlparse
|
|||||||
import secp256k1
|
import secp256k1
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.legacy.client import connect as ws_connect
|
from websockets import connect as ws_connect
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import (
|
from lnbits.utils.nostr import (
|
||||||
@@ -62,7 +62,7 @@ class NWCWallet(Wallet):
|
|||||||
# interval in seconds between checks for pending payments
|
# interval in seconds between checks for pending payments
|
||||||
self.pending_payments_lookup_interval = 10
|
self.pending_payments_lookup_interval = 10
|
||||||
# track paid invoices for paid_invoices_stream
|
# track paid invoices for paid_invoices_stream
|
||||||
self.paid_invoices_queue: asyncio.Queue = asyncio.Queue(0)
|
self.paid_invoices_queue = asyncio.Queue(0)
|
||||||
# This task periodically checks if pending payments have been settled
|
# This task periodically checks if pending payments have been settled
|
||||||
self.pending_payments_lookup_task = asyncio.create_task(
|
self.pending_payments_lookup_task = asyncio.create_task(
|
||||||
self._handle_pending_payments()
|
self._handle_pending_payments()
|
||||||
@@ -309,7 +309,8 @@ class NWCConnection:
|
|||||||
self.account_private_key = secp256k1.PrivateKey(bytes.fromhex(secret))
|
self.account_private_key = secp256k1.PrivateKey(bytes.fromhex(secret))
|
||||||
self.account_private_key_hex = secret
|
self.account_private_key_hex = secret
|
||||||
self.account_public_key = self.account_private_key.pubkey
|
self.account_public_key = self.account_private_key.pubkey
|
||||||
assert self.account_public_key
|
if not self.account_public_key:
|
||||||
|
raise ValueError("Missing account public key")
|
||||||
self.account_public_key_hex = self.account_public_key.serialize().hex()[2:]
|
self.account_public_key_hex = self.account_public_key.serialize().hex()[2:]
|
||||||
|
|
||||||
# Extract service key (used for encryption to identify the nwc service provider)
|
# Extract service key (used for encryption to identify the nwc service provider)
|
||||||
@@ -390,7 +391,7 @@ class NWCConnection:
|
|||||||
n = max_length - len(subid)
|
n = max_length - len(subid)
|
||||||
if n > 0:
|
if n > 0:
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
subid += chars[random.randint(0, len(chars) - 1)]
|
subid += chars[random.randint(0, len(chars) - 1)] # noqa: S311
|
||||||
return subid
|
return subid
|
||||||
|
|
||||||
async def _close_subscription_by_subid(
|
async def _close_subscription_by_subid(
|
||||||
@@ -513,7 +514,7 @@ class NWCConnection:
|
|||||||
if subscription: # Check if the subscription exists first
|
if subscription: # Check if the subscription exists first
|
||||||
subscription["future"].set_exception(Exception(info))
|
subscription["future"].set_exception(Exception(info))
|
||||||
|
|
||||||
async def _on_event_message(self, msg: list[Union[str, dict]]):
|
async def _on_event_message(self, msg: list[Union[str, dict]]): # noqa: C901
|
||||||
"""
|
"""
|
||||||
Handles EVENT messages from the relay.
|
Handles EVENT messages from the relay.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+86
-31
@@ -7,14 +7,16 @@ from collections.abc import AsyncGenerator
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from httpx import RequestError, TimeoutException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from websockets.legacy.client import connect
|
from websockets import connect
|
||||||
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -173,13 +175,29 @@ class PhoenixdWallet(Wallet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
r.raise_for_status()
|
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()
|
data = r.json()
|
||||||
|
|
||||||
if "routingFeeSat" not in data and "reason" in data:
|
if "routingFeeSat" not in data and ("reason" in data or "message" in data):
|
||||||
return PaymentResponse(error_message=data["reason"])
|
error_message = data.get("reason", data.get("message", "Unknown error"))
|
||||||
|
|
||||||
if r.is_error or "paymentHash" not in data:
|
|
||||||
error_message = data["message"] if "message" in data else r.text
|
|
||||||
return PaymentResponse(error_message=error_message)
|
return PaymentResponse(error_message=error_message)
|
||||||
|
|
||||||
checking_id = data["paymentHash"]
|
checking_id = data["paymentHash"]
|
||||||
@@ -208,46 +226,83 @@ class PhoenixdWallet(Wallet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||||
try:
|
r = await self.client.get(
|
||||||
r = await self.client.get(f"/payments/incoming/{checking_id}")
|
f"/payments/incoming/{checking_id}?all=true&limit=1000"
|
||||||
if r.is_error:
|
)
|
||||||
|
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()
|
return PaymentPendingStatus()
|
||||||
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
if data["isPaid"]:
|
# should never return invalid json, but just in case we keep it pending
|
||||||
fee_msat = data["fees"]
|
logger.warning(
|
||||||
preimage = data["preimage"]
|
f"Phoenixd: invalid json response: {r.text}, keeping pending"
|
||||||
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
|
)
|
||||||
|
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting invoice status: {e}")
|
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
|
||||||
return PaymentPendingStatus()
|
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:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
# TODO: how can we detect a failed payment?
|
r = await self.client.get(
|
||||||
try:
|
f"/payments/outgoing/{checking_id}?all=true&limit=1000"
|
||||||
r = await self.client.get(f"/payments/outgoing/{checking_id}")
|
)
|
||||||
if r.is_error:
|
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()
|
return PaymentPendingStatus()
|
||||||
|
try:
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
if data["isPaid"]:
|
# should never return invalid json, but just in case we keep it pending
|
||||||
fee_msat = data["fees"]
|
logger.warning(
|
||||||
preimage = data["preimage"]
|
f"Phoenixd: invalid json response: {r.text}, keeping pending"
|
||||||
return PaymentSuccessStatus(fee_msat=fee_msat, preimage=preimage)
|
)
|
||||||
|
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting invoice status: {e}")
|
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()
|
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 paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
try:
|
try:
|
||||||
async with connect(
|
async with connect(
|
||||||
self.ws_url,
|
self.ws_url,
|
||||||
extra_headers=[("Authorization", self.headers["Authorization"])],
|
additional_headers=[
|
||||||
|
("Authorization", self.headers["Authorization"])
|
||||||
|
],
|
||||||
) as ws:
|
) as ws:
|
||||||
logger.info("connected to phoenixd invoices stream")
|
logger.info("connected to phoenixd invoices stream")
|
||||||
while settings.lnbits_running:
|
while settings.lnbits_running:
|
||||||
@@ -263,7 +318,7 @@ class PhoenixdWallet(Wallet):
|
|||||||
yield message_json["paymentHash"]
|
yield message_json["paymentHash"]
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(
|
logger.warning(
|
||||||
f"lost connection to phoenixd invoices stream: '{exc}'"
|
f"lost connection to phoenixd invoices stream: '{exc}'"
|
||||||
"retrying in 5 seconds"
|
"retrying in 5 seconds"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from secrets import token_urlsafe
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -116,7 +116,7 @@ class SparkWallet(Wallet):
|
|||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: Optional[bytes] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = f"lbs{random.random()}"
|
label = f"lbs{token_urlsafe(16)}"
|
||||||
try:
|
try:
|
||||||
if description_hash:
|
if description_hash:
|
||||||
r = await self.invoicewithdescriptionhash(
|
r = await self.invoicewithdescriptionhash(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TokenBucket:
|
|||||||
Token bucket rate limiter for Strike API endpoints.
|
Token bucket rate limiter for Strike API endpoints.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rate: int, period_seconds: int):
|
def __init__(self, rate: int, period_seconds: int) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a token bucket.
|
Initialize a token bucket.
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class StrikeWallet(Wallet):
|
|||||||
# construction / teardown #
|
# construction / teardown #
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
if not settings.strike_api_endpoint:
|
if not settings.strike_api_endpoint:
|
||||||
raise ValueError("Missing strike_api_endpoint")
|
raise ValueError("Missing strike_api_endpoint")
|
||||||
if not settings.strike_api_key:
|
if not settings.strike_api_key:
|
||||||
@@ -120,7 +120,7 @@ class StrikeWallet(Wallet):
|
|||||||
self._cached_balance_ts: float = 0.0
|
self._cached_balance_ts: float = 0.0
|
||||||
self._cache_ttl = 30 # seconds
|
self._cache_ttl = 30 # seconds
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self) -> None:
|
||||||
try:
|
try:
|
||||||
await self.client.aclose()
|
await self.client.aclose()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -487,7 +487,7 @@ class StrikeWallet(Wallet):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _get_payment_status_by_checking_id(
|
async def _get_payment_status_by_checking_id( # noqa: C901
|
||||||
self, checking_id: str
|
self, checking_id: str
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
r_payment = await self._get(f"/payments/{checking_id}")
|
r_payment = await self._get(f"/payments/{checking_id}")
|
||||||
|
|||||||
Generated
+202
-21
@@ -15,7 +15,7 @@
|
|||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.6",
|
"vue-i18n": "^10.0.8",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "10.0.6",
|
"version": "10.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
|
||||||
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
|
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/message-compiler": "10.0.6",
|
"@intlify/message-compiler": "10.0.8",
|
||||||
"@intlify/shared": "10.0.6"
|
"@intlify/shared": "10.0.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -88,12 +88,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "10.0.6",
|
"version": "10.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
|
||||||
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
|
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "10.0.6",
|
"@intlify/shared": "10.0.8",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "10.0.6",
|
"version": "10.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
|
||||||
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
|
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -448,6 +448,19 @@
|
|||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camel-case": {
|
"node_modules/camel-case": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
||||||
@@ -589,6 +602,20 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -600,6 +627,51 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@@ -637,12 +709,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -663,6 +738,52 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
@@ -675,6 +796,57 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-minifier-terser": {
|
"node_modules/html-minifier-terser": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||||
@@ -776,6 +948,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -1305,13 +1486,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "10.0.6",
|
"version": "10.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
|
||||||
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
|
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "10.0.6",
|
"@intlify/core-base": "10.0.8",
|
||||||
"@intlify/shared": "10.0.6",
|
"@intlify/shared": "10.0.8",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.6",
|
"vue-i18n": "^10.0.8",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
|
|||||||
Generated
+290
-96
@@ -378,6 +378,71 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"]
|
tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"},
|
||||||
|
{file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
||||||
|
typecheck = ["mypy"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bech32"
|
name = "bech32"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -390,6 +455,21 @@ files = [
|
|||||||
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bip32"
|
||||||
|
version = "4.0"
|
||||||
|
description = "Minimalistic implementation of the BIP32 key derivation scheme"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9"},
|
||||||
|
{file = "bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
coincurve = ">=15.0,<21"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitarray"
|
name = "bitarray"
|
||||||
version = "3.4.3"
|
version = "3.4.3"
|
||||||
@@ -656,6 +736,47 @@ files = [
|
|||||||
{file = "breez_sdk-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e51689ed2f2c2d49bdb53f0ee26d0dc2109a50f15b8f2129a250db275eb994bf"},
|
{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]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.6.15"
|
version = "2025.6.15"
|
||||||
@@ -1250,24 +1371,25 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.13"
|
version = "0.116.1"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865"},
|
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
|
||||||
{file = "fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307"},
|
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||||
starlette = ">=0.40.0,<0.47.0"
|
starlette = ">=0.40.0,<0.48.0"
|
||||||
typing-extensions = ">=4.8.0"
|
typing-extensions = ">=4.8.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-sso"
|
name = "fastapi-sso"
|
||||||
@@ -2011,21 +2133,23 @@ valkey = ["valkey (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnurl"
|
name = "lnurl"
|
||||||
version = "0.5.3"
|
version = "0.7.2"
|
||||||
description = "LNURL implementation for Python."
|
description = "LNURL implementation for Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.9"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "lnurl-0.5.3-py3-none-any.whl", hash = "sha256:feaf6c60b0b7f104894ef3accbd30d23d52e038c2797c58432baea7f4a8aa952"},
|
{file = "lnurl-0.7.2-py3-none-any.whl", hash = "sha256:4323ac398d49e5b883000c166b4c2448df2acad2eddca99f656c67f7b97cd80f"},
|
||||||
{file = "lnurl-0.5.3.tar.gz", hash = "sha256:60154bcfdbb98fb143eeca970a16d73a582f28e057a826b5f222259411c497fe"},
|
{file = "lnurl-0.7.2.tar.gz", hash = "sha256:9f0881cb5909512cadb35d238c27347d6a8afcec10b2b7aadd560083fd210c0b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
bech32 = ">=1.2.0,<2.0.0"
|
bech32 = "*"
|
||||||
bolt11 = ">=2.0.5,<3.0.0"
|
bip32 = ">=4.0,<5.0"
|
||||||
ecdsa = ">=0.19.0,<0.20.0"
|
bolt11 = "*"
|
||||||
httpx = ">=0.27.0,<0.28.0"
|
ecdsa = "*"
|
||||||
|
httpx = "*"
|
||||||
|
pycryptodomex = ">=3.21.0,<4.0.0"
|
||||||
pydantic = ">=1,<2"
|
pydantic = ">=1,<2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2498,24 +2622,6 @@ files = [
|
|||||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "passlib"
|
|
||||||
version = "1.7.4"
|
|
||||||
description = "comprehensive password hashing framework supporting over 30 schemes"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
groups = ["main"]
|
|
||||||
files = [
|
|
||||||
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
|
||||||
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
argon2 = ["argon2-cffi (>=18.2.0)"]
|
|
||||||
bcrypt = ["bcrypt (>=3.1.0)"]
|
|
||||||
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
|
||||||
totp = ["cryptography"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathable"
|
name = "pathable"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -2714,23 +2820,102 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "5.29.1"
|
version = "5.29.5"
|
||||||
description = ""
|
description = ""
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110"},
|
{file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"},
|
||||||
{file = "protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34"},
|
{file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"},
|
||||||
{file = "protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18"},
|
{file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"},
|
||||||
{file = "protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155"},
|
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"},
|
||||||
{file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"},
|
{file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"},
|
||||||
{file = "protobuf-5.29.1-cp38-cp38-win32.whl", hash = "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853"},
|
{file = "protobuf-5.29.5-cp38-cp38-win32.whl", hash = "sha256:ef91363ad4faba7b25d844ef1ada59ff1604184c0bcd8b39b8a6bef15e1af238"},
|
||||||
{file = "protobuf-5.29.1-cp38-cp38-win_amd64.whl", hash = "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331"},
|
{file = "protobuf-5.29.5-cp38-cp38-win_amd64.whl", hash = "sha256:7318608d56b6402d2ea7704ff1e1e4597bee46d760e7e4dd42a3d45e24b87f2e"},
|
||||||
{file = "protobuf-5.29.1-cp39-cp39-win32.whl", hash = "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57"},
|
{file = "protobuf-5.29.5-cp39-cp39-win32.whl", hash = "sha256:6f642dc9a61782fa72b90878af134c5afe1917c89a568cd3476d758d3c3a0736"},
|
||||||
{file = "protobuf-5.29.1-cp39-cp39-win_amd64.whl", hash = "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c"},
|
{file = "protobuf-5.29.5-cp39-cp39-win_amd64.whl", hash = "sha256:470f3af547ef17847a28e1f47200a1cbf0ba3ff57b7de50d22776607cd2ea353"},
|
||||||
{file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"},
|
{file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"},
|
||||||
{file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"},
|
{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"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2813,55 +2998,62 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "1.10.18"
|
version = "1.10.22"
|
||||||
description = "Data validation and settings management using python type hints"
|
description = "Data validation and settings management using python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
|
{file = "pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
|
{file = "pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
|
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
|
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8dad498ceff2d9ef1d2e2bc6608f5b59b8e1ba2031759b22dfb8c16608e1802"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
|
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fac529cc654d4575cf8de191cce354b12ba705f528a0a5c654de6d01f76cd818"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
|
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
|
{file = "pydantic-1.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:ece68105d9e436db45d8650dc375c760cc85a6793ae019c08769052902dca7db"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
|
{file = "pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
|
{file = "pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
|
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
|
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
|
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
|
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
|
{file = "pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
|
{file = "pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
|
{file = "pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
|
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
|
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
|
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
|
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
|
{file = "pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
|
{file = "pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
|
{file = "pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
|
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
|
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
|
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
|
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
|
{file = "pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
|
{file = "pydantic-1.10.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ecaf8177b06aac5d1f442db1288e3b46d9f05f34fd17fdca3ad34105328b61a"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
|
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb36c2de9ea74bd7f66b5481dea8032d399affd1cbfbb9bb7ce539437f1fce62"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
|
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6b8d14a256be3b8fff9286d76c532f1a7573fbba5f189305b22471c6679854d"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
|
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1c33269e815db4324e71577174c29c7aa30d1bba51340ce6be976f6f3053a4c6"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
|
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:8661b3ab2735b2a9ccca2634738534a795f4a10bae3ab28ec0a10c96baa20182"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
|
{file = "pydantic-1.10.22-cp37-cp37m-win_amd64.whl", hash = "sha256:22bdd5fe70d4549995981c55b970f59de5c502d5656b2abdfcd0a25be6f3763e"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
|
{file = "pydantic-1.10.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e3f33d1358aa4bc2795208cc29ff3118aeaad0ea36f0946788cf7cadeccc166b"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
|
{file = "pydantic-1.10.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:813f079f9cd136cac621f3f9128a4406eb8abd2ad9fdf916a0731d91c6590017"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
|
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab618ab8dca6eac7f0755db25f6aba3c22c40e3463f85a1c08dc93092d917704"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
|
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d128e1aaa38db88caca920d5822c98fc06516a09a58b6d3d60fa5ea9099b32cc"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
|
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc97bbc25def7025e55fc9016080773167cda2aad7294e06a37dda04c7d69ece"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
|
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dda5d7157d543b1fa565038cae6e952549d0f90071c839b3740fb77c820fab8"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
|
{file = "pydantic-1.10.22-cp38-cp38-win_amd64.whl", hash = "sha256:a093fe44fe518cb445d23119511a71f756f8503139d02fcdd1173f7b76c95ffe"},
|
||||||
{file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
|
{file = "pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587"},
|
||||||
{file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
|
{file = "pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53cdb44b78c420f570ff16b071ea8cd5a477635c6b0efc343c8a91e3029bbf1a"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16d0a5ae9d98264186ce31acdd7686ec05fd331fab9d68ed777d5cb2d1514e5e"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:7f691eec68dbbfca497d3c11b92a3e5987393174cbedf03ec7a4184c35c2def6"},
|
||||||
|
{file = "pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9"},
|
||||||
|
{file = "pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -3696,21 +3888,22 @@ uvicorn = ["uvicorn (>=0.34.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.40.0"
|
version = "0.47.1"
|
||||||
description = "The little ASGI library that shines."
|
description = "The little ASGI library that shines."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
|
{file = "starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527"},
|
||||||
{file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
|
{file = "starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.4.0,<5"
|
anyio = ">=3.6.2,<5"
|
||||||
|
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tlv8"
|
name = "tlv8"
|
||||||
@@ -4373,10 +4566,11 @@ multidict = ">=4.0"
|
|||||||
propcache = ">=0.2.1"
|
propcache = ">=0.2.1"
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk", "breez-sdk-liquid"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
|
migration = ["psycopg2-binary"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "~3.12 | ~3.11 | ~3.10"
|
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||||
content-hash = "434e37e5aeb0eeb1a22e12fd1914b45f27195162ef4ebc1055071fd4718d85fc"
|
content-hash = "7100b67d94f82cb53ceb8f4e47ae8a4aa426408feabeb0746c323d09f229c242"
|
||||||
|
|||||||
+36
-10
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.2.1"
|
version = "1.3.0-rc3"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -16,11 +16,12 @@ python = "~3.12 | ~3.11 | ~3.10"
|
|||||||
bech32 = "1.2.0"
|
bech32 = "1.2.0"
|
||||||
click = "8.2.1"
|
click = "8.2.1"
|
||||||
ecdsa = "0.19.1"
|
ecdsa = "0.19.1"
|
||||||
fastapi = "0.115.13"
|
fastapi = "0.116.1"
|
||||||
|
starlette = "0.47.1"
|
||||||
httpx = "0.27.0"
|
httpx = "0.27.0"
|
||||||
jinja2 = "3.1.6"
|
jinja2 = "3.1.6"
|
||||||
lnurl = "0.5.3"
|
lnurl = "0.7.2"
|
||||||
pydantic = "1.10.18"
|
pydantic = "1.10.22"
|
||||||
pyqrcode = "1.2.1"
|
pyqrcode = "1.2.1"
|
||||||
shortuuid = "1.0.13"
|
shortuuid = "1.0.13"
|
||||||
sse-starlette = "2.3.6"
|
sse-starlette = "2.3.6"
|
||||||
@@ -33,7 +34,7 @@ uvloop = "0.21.0"
|
|||||||
websockets = "15.0.1"
|
websockets = "15.0.1"
|
||||||
loguru = "0.7.3"
|
loguru = "0.7.3"
|
||||||
grpcio = "1.69.0"
|
grpcio = "1.69.0"
|
||||||
protobuf = "5.29.1"
|
protobuf = "5.29.5"
|
||||||
pyln-client = "25.5"
|
pyln-client = "25.5"
|
||||||
pywebpush = "2.0.3"
|
pywebpush = "2.0.3"
|
||||||
slowapi = "0.1.9"
|
slowapi = "0.1.9"
|
||||||
@@ -42,7 +43,6 @@ pycryptodomex = "3.23.0"
|
|||||||
packaging = "25.0"
|
packaging = "25.0"
|
||||||
bolt11 = "2.1.1"
|
bolt11 = "2.1.1"
|
||||||
pyjwt = "2.10.1"
|
pyjwt = "2.10.1"
|
||||||
passlib = "1.7.4"
|
|
||||||
itsdangerous = "2.2.0"
|
itsdangerous = "2.2.0"
|
||||||
fastapi-sso = "0.18.0"
|
fastapi-sso = "0.18.0"
|
||||||
# needed for boltz, lnurldevice, watchonly extensions
|
# needed for boltz, lnurldevice, watchonly extensions
|
||||||
@@ -57,16 +57,21 @@ python-crontab = "3.2.0"
|
|||||||
wallycore = {version = "1.4.0", optional = true}
|
wallycore = {version = "1.4.0", optional = true}
|
||||||
# needed for breez funding source
|
# needed for breez funding source
|
||||||
breez-sdk = {version = "0.8.0", optional = true}
|
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"
|
jsonpath-ng = "^1.7.0"
|
||||||
pynostr = "^0.6.2"
|
pynostr = "^0.6.2"
|
||||||
python-multipart = "^0.0.20"
|
python-multipart = "^0.0.20"
|
||||||
filetype = "^1.2.0"
|
filetype = "^1.2.0"
|
||||||
nostr-sdk = "^0.42.1"
|
nostr-sdk = "^0.42.1"
|
||||||
|
bcrypt = "^4.3.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk", "breez-sdk-liquid"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
|
migration = ["psycopg2-binary"]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^25.1.0"
|
black = "^25.1.0"
|
||||||
@@ -147,6 +152,8 @@ module = [
|
|||||||
"json5.*",
|
"json5.*",
|
||||||
"jsonpath_ng.*",
|
"jsonpath_ng.*",
|
||||||
"filetype.*",
|
"filetype.*",
|
||||||
|
"breez_sdk.*",
|
||||||
|
"breez_sdk_liquid.*",
|
||||||
]
|
]
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
|
|
||||||
@@ -191,7 +198,8 @@ extend-exclude = [
|
|||||||
# UP - pyupgrade
|
# UP - pyupgrade
|
||||||
# RUF - ruff specific rules
|
# RUF - ruff specific rules
|
||||||
# B - bugbear
|
# B - bugbear
|
||||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
# S - bandit
|
||||||
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B", "S"]
|
||||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||||
# RUF012: mutable-class-default
|
# RUF012: mutable-class-default
|
||||||
ignore = ["RUF012"]
|
ignore = ["RUF012"]
|
||||||
@@ -210,9 +218,27 @@ classmethod-decorators = [
|
|||||||
"validator",
|
"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]
|
[tool.ruff.lint.mccabe]
|
||||||
# TODO: Decrease this to 10.
|
max-complexity = 10
|
||||||
max-complexity = 16
|
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-bugbear]
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user