Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13426a56b | ||
|
|
6aaa46e5a0 | ||
|
|
b9db3eafe0 | ||
|
|
ae9eb314cc | ||
|
|
fe393b83ab | ||
|
|
bc55d52ea2 | ||
|
|
9a0878de19 | ||
|
|
be7c589722 | ||
|
|
28a55b2d41 | ||
|
|
d66c83c91a | ||
|
|
7e1f43933d | ||
|
|
758a4ecaf6 | ||
|
|
339645a912 | ||
|
|
9014e73529 | ||
|
|
48fc73d9d4 | ||
|
|
150a33009d | ||
|
|
e6499104c0 | ||
|
|
49411e58cc | ||
|
|
8c0e7725de | ||
|
|
95281eba8c | ||
|
|
cd17b59a3a | ||
|
|
ec623f4861 | ||
|
|
7c479a3ebf | ||
|
|
9017cee491 | ||
|
|
2940716807 | ||
|
|
c48e2329c5 | ||
|
|
8ed2422ade | ||
|
|
af660fec14 | ||
|
|
6bbebe0cee | ||
|
|
f863a043cb | ||
|
|
2216649412 | ||
|
|
9d8c78c927 | ||
|
|
02bf2f9819 | ||
|
|
921984e52b | ||
|
|
d0599458c6 | ||
|
|
80b803913f | ||
|
|
6eab3dae01 | ||
|
|
eab19280d5 | ||
|
|
c960f718f5 | ||
|
|
7df131d3c7 | ||
|
|
cae24b233e | ||
|
|
252fd6c313 | ||
|
|
37ac630573 | ||
|
|
035d6263f9 | ||
|
|
0ab1924327 | ||
|
|
127b780af7 | ||
|
|
39066e4bc8 |
@@ -8,6 +8,12 @@ PORT=5000
|
||||
|
||||
DEBUG=false
|
||||
|
||||
# Server security, rate limiting ips, blocked ips, allowed ips
|
||||
LNBITS_RATE_LIMIT_NO="200"
|
||||
LNBITS_RATE_LIMIT_UNIT="minute"
|
||||
LNBITS_ALLOWED_IPS=""
|
||||
LNBITS_BLOCKED_IPS=""
|
||||
|
||||
# Allow users and admins by user IDs (comma separated list)
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
@@ -52,6 +58,9 @@ LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
|
||||
# Limit fiat currencies allowed to see in UI
|
||||
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
|
||||
@@ -38,8 +38,8 @@ jobs:
|
||||
LNBITS_DATA_FOLDER: ./data
|
||||
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
|
||||
LND_REST_ENDPOINT: https://localhost:8081/
|
||||
LND_REST_CERT: ./docker/data/lnd-1/tls.cert
|
||||
LND_REST_MACAROON: ./docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
|
||||
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
run: |
|
||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||
make test-real-wallet
|
||||
@@ -83,8 +83,8 @@ jobs:
|
||||
LNBITS_BACKEND_WALLET_CLASS: LndWallet
|
||||
LND_GRPC_ENDPOINT: localhost
|
||||
LND_GRPC_PORT: 10009
|
||||
LND_GRPC_CERT: docker/data/lnd-1/tls.cert
|
||||
LND_GRPC_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
|
||||
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
|
||||
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||
run: |
|
||||
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
|
||||
make test-real-wallet
|
||||
|
||||
@@ -24,6 +24,7 @@ tests/data/*.sqlite3
|
||||
*.env
|
||||
.env
|
||||
.super_user
|
||||
.pre-commit-config.yaml
|
||||
|
||||
data
|
||||
*.sqlite3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/extensions/.*|^lnbits/wallets/lnd_grpc_files/.*|^package-lock.json$'
|
||||
exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/extensions/.*|^lnbits/upgrades/.*|^lnbits/wallets/lnd_grpc_files/.*|^package-lock.json$'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
@@ -26,17 +26,10 @@ repos:
|
||||
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [css, javascript, html, json]
|
||||
args: ['lnbits']
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
entry: poetry run flake8
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.971
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
entry: poetry run mypy
|
||||
args: [--ignore-missing-imports]
|
||||
additional_dependencies: [types-mock, types-protobuf]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*.yml
|
||||
|
||||
**/lnbits/extensions/*
|
||||
**/lnbits/upgrades/*
|
||||
|
||||
**/lnbits/static/vendor
|
||||
**/lnbits/static/bundle.*
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.10-slim-bullseye
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
|
||||
@@ -12,6 +12,15 @@ For developers
|
||||
Thanks for contributing :)
|
||||
|
||||
|
||||
Precommit hooks
|
||||
=====
|
||||
|
||||
This ensures that all commits adhere to the formatting and linting rules.
|
||||
|
||||
```bash
|
||||
make install-pre-commit-hook
|
||||
```
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
|
||||
@@ -55,11 +55,12 @@ Now start LNbits once in the terminal window
|
||||
```
|
||||
$ poetry run lnbits
|
||||
```
|
||||
It will now show you the Super User Account:
|
||||
|
||||
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
|
||||
|
||||
The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain.
|
||||
You can now `cat` the Super User ID:
|
||||
```
|
||||
$ cat .super_user
|
||||
123de4bfdddddbbeb48c8bc8382fe123
|
||||
```
|
||||
You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain.
|
||||
|
||||
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ A backend wallet can be configured using the following LNbits environment variab
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
|
||||
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
|
||||
|
||||
### Spark (c-lightning)
|
||||
### Spark (Core Lightning)
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
|
||||
- `SPARK_URL`: http://10.147.17.230:9737/rpc
|
||||
|
||||
+82
-17
@@ -7,21 +7,30 @@ import shutil
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lnbits.core.crud import get_installed_extensions
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.tasks import register_task_listeners
|
||||
from lnbits.settings import get_wallet_class, set_wallet_class, settings
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog,
|
||||
register_killswitch,
|
||||
register_task_listeners,
|
||||
unregister_killswitch,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
|
||||
from .commands import db_versions, load_disabled_extension_list, migrate_databases
|
||||
from .core import (
|
||||
@@ -34,7 +43,12 @@ from .core.services import check_admin_settings
|
||||
from .core.views.generic import core_html_routes
|
||||
from .extension_manager import Extension, InstallableExtension, get_valid_extensions
|
||||
from .helpers import template_renderer
|
||||
from .middleware import ExtensionsRedirectMiddleware, InstalledExtensionMiddleware
|
||||
from .middleware import (
|
||||
ExtensionsRedirectMiddleware,
|
||||
InstalledExtensionMiddleware,
|
||||
add_ip_block_middleware,
|
||||
add_ratelimit_middleware,
|
||||
)
|
||||
from .requestvars import g
|
||||
from .tasks import (
|
||||
catch_everything_and_restart,
|
||||
@@ -46,9 +60,7 @@ from .tasks import (
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
|
||||
configure_logger()
|
||||
|
||||
app = FastAPI(
|
||||
title="LNbits API",
|
||||
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
|
||||
@@ -82,15 +94,16 @@ def create_app() -> FastAPI:
|
||||
register_routes(app)
|
||||
register_async_tasks(app)
|
||||
register_exception_handlers(app)
|
||||
register_shutdown(app)
|
||||
|
||||
# Allow registering new extensions routes without direct access to the `app` object
|
||||
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
|
||||
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
|
||||
|
||||
return app
|
||||
|
||||
|
||||
async def check_funding_source() -> None:
|
||||
|
||||
original_sigint_handler = signal.getsignal(signal.SIGINT)
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
@@ -154,13 +167,17 @@ async def check_installed_extensions(app: FastAPI):
|
||||
|
||||
for ext in installed_extensions:
|
||||
try:
|
||||
installed = check_installed_extension(ext)
|
||||
installed = check_installed_extension_files(ext)
|
||||
if not installed:
|
||||
await restore_installed_extension(app, ext)
|
||||
logger.info(f"✔️ Successfully re-installed extension: {ext.id}")
|
||||
logger.info(
|
||||
f"✔️ Successfully re-installed extension: {ext.id} ({ext.installed_version})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
logger.warning(f"Failed to re-install extension: {ext.id}")
|
||||
logger.warning(
|
||||
f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
|
||||
)
|
||||
|
||||
|
||||
async def build_all_installed_extensions_list() -> List[InstallableExtension]:
|
||||
@@ -183,13 +200,11 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]:
|
||||
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
|
||||
)
|
||||
installed_extensions.append(ext_info)
|
||||
await add_installed_extension(ext_info)
|
||||
await update_installed_extension_state(ext_id=ext_id, active=True)
|
||||
|
||||
return installed_extensions
|
||||
|
||||
|
||||
def check_installed_extension(ext: InstallableExtension) -> bool:
|
||||
def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
||||
if ext.has_installed_version:
|
||||
return True
|
||||
|
||||
@@ -205,6 +220,9 @@ def check_installed_extension(ext: InstallableExtension) -> bool:
|
||||
|
||||
|
||||
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
||||
await add_installed_extension(ext)
|
||||
await update_installed_extension_state(ext_id=ext.id, active=True)
|
||||
|
||||
extension = Extension.from_installable_ext(ext)
|
||||
register_ext_routes(app, extension)
|
||||
|
||||
@@ -241,6 +259,19 @@ def register_new_ext_routes(app: FastAPI) -> Callable:
|
||||
return register_new_ext_routes_fn
|
||||
|
||||
|
||||
def register_new_ratelimiter(app: FastAPI) -> Callable:
|
||||
def register_new_ratelimiter_fn():
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
default_limits=[
|
||||
f"{settings.lnbits_rate_limit_no}/{settings.lnbits_rate_limit_unit}"
|
||||
],
|
||||
)
|
||||
app.state.limiter = limiter
|
||||
|
||||
return register_new_ratelimiter_fn
|
||||
|
||||
|
||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
"""Register FastAPI routes for extension."""
|
||||
ext_module = importlib.import_module(ext.module_name)
|
||||
@@ -274,7 +305,6 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||
def register_startup(app: FastAPI):
|
||||
@app.on_event("startup")
|
||||
async def lnbits_startup():
|
||||
|
||||
try:
|
||||
# wait till migration is done
|
||||
await migrate_databases()
|
||||
@@ -284,6 +314,10 @@ def register_startup(app: FastAPI):
|
||||
|
||||
log_server_info()
|
||||
|
||||
# adds security middleware
|
||||
add_ratelimit_middleware(app)
|
||||
add_ip_block_middleware(app)
|
||||
|
||||
# initialize WALLET
|
||||
set_wallet_class()
|
||||
|
||||
@@ -293,11 +327,39 @@ def register_startup(app: FastAPI):
|
||||
# check extensions after restart
|
||||
await check_installed_extensions(app)
|
||||
|
||||
if settings.lnbits_admin_ui:
|
||||
initialize_server_logger()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
raise ImportError("Failed to run 'startup' event.")
|
||||
|
||||
|
||||
def register_shutdown(app: FastAPI):
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown():
|
||||
WALLET = get_wallet_class()
|
||||
await WALLET.cleanup()
|
||||
|
||||
|
||||
def initialize_server_logger():
|
||||
super_user_hash = sha256(settings.super_user.encode("utf-8")).hexdigest()
|
||||
|
||||
serverlog_queue = asyncio.Queue()
|
||||
|
||||
async def update_websocket_serverlog():
|
||||
while True:
|
||||
msg = await serverlog_queue.get()
|
||||
await websocketUpdater(super_user_hash, msg)
|
||||
|
||||
asyncio.create_task(update_websocket_serverlog())
|
||||
|
||||
logger.add(
|
||||
lambda msg: serverlog_queue.put_nowait(msg),
|
||||
format=Formatter().format,
|
||||
)
|
||||
|
||||
|
||||
def log_server_info():
|
||||
logger.info("Starting LNbits")
|
||||
logger.info(f"Version: {settings.version}")
|
||||
@@ -336,10 +398,14 @@ def register_async_tasks(app):
|
||||
loop.create_task(catch_everything_and_restart(invoice_listener))
|
||||
loop.create_task(catch_everything_and_restart(internal_invoice_listener))
|
||||
await register_task_listeners()
|
||||
# await register_watchdog()
|
||||
await register_killswitch()
|
||||
# await run_deferred_async() # calle: doesn't do anyting?
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def stop_listeners():
|
||||
# await unregister_watchdog()
|
||||
await unregister_killswitch()
|
||||
pass
|
||||
|
||||
|
||||
@@ -418,7 +484,6 @@ def configure_logger() -> None:
|
||||
log_level: str = "DEBUG" if settings.debug else "INFO"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stderr, level=log_level, format=formatter.format)
|
||||
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
|
||||
|
||||
+36
-2
@@ -7,7 +7,7 @@ from uuid import UUID, uuid4
|
||||
import shortuuid
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.db import Connection, Database, Filters, Page
|
||||
from lnbits.extension_manager import InstallableExtension
|
||||
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
|
||||
|
||||
@@ -75,6 +75,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
|
||||
wallets=[Wallet(**w) for w in wallets],
|
||||
admin=user["id"] == settings.super_user
|
||||
or user["id"] in settings.lnbits_admin_users,
|
||||
super_user=user["id"] == settings.super_user,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,6 +143,25 @@ async def delete_installed_extension(
|
||||
)
|
||||
|
||||
|
||||
async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||
db_version = await (conn or db).fetchone(
|
||||
"SELECT * FROM dbversions WHERE db = ?", (ext_id,)
|
||||
)
|
||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||
assert db_version, f"Extension '{ext_id}' db version cannot be found"
|
||||
|
||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||
if is_file_based_db:
|
||||
return
|
||||
|
||||
# String formatting is required, params are not accepted for 'DROP SCHEMA'.
|
||||
# The `ext_id` value is verified above.
|
||||
await (conn or db).execute(
|
||||
f"DROP SCHEMA IF EXISTS {ext_id} CASCADE",
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
|
||||
row = await (conn or db).fetchone(
|
||||
"SELECT * FROM installed_extensions WHERE id = ?",
|
||||
@@ -285,6 +305,11 @@ async def get_total_balance(conn: Optional[Connection] = None):
|
||||
return 0 if row[0] is None else row[0]
|
||||
|
||||
|
||||
async def get_active_wallet_total_balance(conn: Optional[Connection] = None):
|
||||
row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances")
|
||||
return 0 if row[0] is None else row[0]
|
||||
|
||||
|
||||
# wallet payments
|
||||
# ---------------
|
||||
|
||||
@@ -336,7 +361,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
|
||||
rows = await db.fetchall(
|
||||
f"""
|
||||
SELECT * FROM apipayments
|
||||
WHERE pending = 'false'
|
||||
WHERE pending = false
|
||||
AND extra LIKE ?
|
||||
AND extra LIKE ?
|
||||
ORDER BY time DESC LIMIT {limit}
|
||||
@@ -781,6 +806,15 @@ async def update_migration_version(conn, db_name, version):
|
||||
)
|
||||
|
||||
|
||||
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"""
|
||||
DELETE FROM dbversions WHERE db = ?
|
||||
""",
|
||||
(ext_id,),
|
||||
)
|
||||
|
||||
|
||||
# tinyurl
|
||||
# -------
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection, FilterModel, FromRowModel
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
|
||||
|
||||
@@ -37,7 +38,6 @@ class Wallet(BaseModel):
|
||||
|
||||
@property
|
||||
def lnurlwithdraw_full(self) -> str:
|
||||
|
||||
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
||||
try:
|
||||
return lnurl_encode(url)
|
||||
@@ -244,6 +244,7 @@ class BalanceCheck(BaseModel):
|
||||
|
||||
class CoreAppExtra:
|
||||
register_new_ext_routes: Callable
|
||||
register_new_ratelimiter: Callable
|
||||
|
||||
|
||||
class TinyURL(BaseModel):
|
||||
|
||||
+30
-10
@@ -15,14 +15,13 @@ from lnbits.db import Connection
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import (
|
||||
FAKE_WALLET,
|
||||
EditableSettings,
|
||||
SuperSettings,
|
||||
get_wallet_class,
|
||||
readonly_variables,
|
||||
send_admin_user_to_saas,
|
||||
settings,
|
||||
)
|
||||
from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class
|
||||
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
||||
|
||||
from . import db
|
||||
@@ -37,6 +36,7 @@ from .crud import (
|
||||
get_account,
|
||||
get_standalone_payment,
|
||||
get_super_settings,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_payment,
|
||||
update_payment_details,
|
||||
@@ -83,7 +83,7 @@ async def create_invoice(
|
||||
unhashed_description=unhashed_description,
|
||||
expiry=expiry or settings.lightning_invoice_expiry,
|
||||
)
|
||||
if not ok:
|
||||
if not ok or not payment_request or not checking_id:
|
||||
raise InvoiceFailure(error_message or "unexpected backend error.")
|
||||
|
||||
invoice = bolt11.decode(payment_request)
|
||||
@@ -247,6 +247,15 @@ async def pay_invoice(
|
||||
new_checking_id=payment.checking_id,
|
||||
conn=conn,
|
||||
)
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if wallet:
|
||||
await websocketUpdater(
|
||||
wallet_id,
|
||||
{
|
||||
"wallet_balance": wallet.balance or None,
|
||||
"payment": payment._asdict(),
|
||||
},
|
||||
)
|
||||
logger.debug(f"payment successful {payment.checking_id}")
|
||||
elif payment.checking_id is None and payment.ok is False:
|
||||
# payment failed
|
||||
@@ -455,12 +464,7 @@ async def check_admin_settings():
|
||||
|
||||
update_cached_settings(settings_db.dict())
|
||||
|
||||
# printing settings for debugging
|
||||
logger.debug("Admin settings:")
|
||||
for key, value in settings.dict(exclude_none=True).items():
|
||||
logger.debug(f"{key}: {value}")
|
||||
|
||||
admin_url = f"{settings.lnbits_baseurl}wallet?usr={settings.super_user}"
|
||||
admin_url = f'{settings.lnbits_baseurl}wallet?usr=<ID from ".super_user" file>'
|
||||
logger.success(f"✔️ Access super user account at: {admin_url}")
|
||||
|
||||
# saving it to .super_user file
|
||||
@@ -507,7 +511,6 @@ class WebsocketConnectionManager:
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
logger.debug(websocket)
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
@@ -524,3 +527,20 @@ websocketManager = WebsocketConnectionManager()
|
||||
|
||||
async def websocketUpdater(item_id, data):
|
||||
return await websocketManager.send_data(f"{data}", item_id)
|
||||
|
||||
|
||||
async def switch_to_voidwallet() -> None:
|
||||
WALLET = get_wallet_class()
|
||||
if WALLET.__class__.__name__ == "VoidWallet":
|
||||
return
|
||||
set_wallet_class("VoidWallet")
|
||||
settings.lnbits_backend_wallet_class = "VoidWallet"
|
||||
|
||||
|
||||
async def get_balance_delta() -> Tuple[int, int, int]:
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
error_message, node_balance = await WALLET.status()
|
||||
if error_message:
|
||||
raise Exception(error_message)
|
||||
return node_balance - total_balance, node_balance, total_balance
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
data: function () {
|
||||
return {
|
||||
searchTerm: '',
|
||||
filteredExtensions: [],
|
||||
maxStars: 5,
|
||||
user: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.filteredExtensions = this.g.extensions
|
||||
},
|
||||
watch: {
|
||||
searchTerm(term) {
|
||||
// Reset the filter
|
||||
this.filteredExtensions = this.g.extensions
|
||||
if (term !== '') {
|
||||
// Filter the extensions list
|
||||
function extensionNameContains(searchTerm) {
|
||||
return function (extension) {
|
||||
return (
|
||||
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
extension.shortDescription
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.filteredExtensions = this.filteredExtensions.filter(
|
||||
extensionNameContains(term)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (window.user) {
|
||||
this.user = LNbits.map.user(window.user)
|
||||
}
|
||||
},
|
||||
mixins: [windowMixin]
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 18
|
||||
const CACHE_VERSION = 40
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
|
||||
@@ -144,6 +144,33 @@ new Vue({
|
||||
},
|
||||
payments: [],
|
||||
paymentsTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'time',
|
||||
align: 'left',
|
||||
label: this.$t('memo') + '/' + this.$t('date'),
|
||||
field: 'date',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
align: 'right',
|
||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: 'sat',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
sortBy: 'time',
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
filter: null,
|
||||
loading: false
|
||||
},
|
||||
paymentsCSV: {
|
||||
columns: [
|
||||
{
|
||||
name: 'memo',
|
||||
@@ -170,15 +197,32 @@ new Vue({
|
||||
align: 'right',
|
||||
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
|
||||
field: 'fee'
|
||||
},
|
||||
{
|
||||
name: 'tag',
|
||||
align: 'right',
|
||||
label: this.$t('tag'),
|
||||
field: 'tag'
|
||||
},
|
||||
{
|
||||
name: 'payment_hash',
|
||||
align: 'right',
|
||||
label: this.$t('payment_hash'),
|
||||
field: 'payment_hash'
|
||||
},
|
||||
{
|
||||
name: 'payment_proof',
|
||||
align: 'right',
|
||||
label: this.$t('payment_proof'),
|
||||
field: 'payment_proof'
|
||||
},
|
||||
{
|
||||
name: 'webhook',
|
||||
align: 'right',
|
||||
label: this.$t('webhook'),
|
||||
field: 'webhook'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10,
|
||||
page: 1,
|
||||
sortBy: 'time',
|
||||
descending: true,
|
||||
rowsNumber: 10
|
||||
},
|
||||
filter: null,
|
||||
loading: false
|
||||
},
|
||||
@@ -743,7 +787,7 @@ new Vue({
|
||||
// status is important for export but it is not in paymentsTable
|
||||
// because it is manually added with payment detail link and icons
|
||||
// and would cause duplication in the list
|
||||
let columns = structuredClone(this.paymentsTable.columns)
|
||||
let columns = structuredClone(this.paymentsCSV.columns)
|
||||
columns.unshift({
|
||||
name: 'pending',
|
||||
align: 'left',
|
||||
|
||||
+94
-5
@@ -1,20 +1,102 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
from typing import Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||
|
||||
from . import db
|
||||
from .crud import get_balance_notify
|
||||
from .crud import get_balance_notify, get_wallet
|
||||
from .models import Payment
|
||||
from .services import websocketUpdater
|
||||
from .services import get_balance_delta, switch_to_voidwallet, websocketUpdater
|
||||
|
||||
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
|
||||
"api_invoice_listeners"
|
||||
)
|
||||
|
||||
killswitch: Optional[asyncio.Task] = None
|
||||
watchdog: Optional[asyncio.Task] = None
|
||||
|
||||
|
||||
async def register_killswitch():
|
||||
"""
|
||||
Registers a killswitch which will check lnbits-status repository
|
||||
for a signal from LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
logger.debug("Starting killswitch task")
|
||||
global killswitch
|
||||
killswitch = asyncio.create_task(killswitch_task())
|
||||
|
||||
|
||||
async def unregister_killswitch():
|
||||
"""
|
||||
Unregisters a killswitch taskl
|
||||
"""
|
||||
global killswitch
|
||||
if killswitch:
|
||||
logger.debug("Stopping killswitch task")
|
||||
killswitch.cancel()
|
||||
|
||||
|
||||
async def killswitch_task():
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet":
|
||||
with httpx.Client() as client:
|
||||
try:
|
||||
r = client.get(settings.lnbits_status_manifest, timeout=4)
|
||||
r.raise_for_status()
|
||||
if r.status_code == 200:
|
||||
ks = r.json().get("killswitch")
|
||||
if ks and ks == 1:
|
||||
logger.error(
|
||||
"Switching to VoidWallet. Killswitch triggered."
|
||||
)
|
||||
await switch_to_voidwallet()
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
logger.error(
|
||||
f"Cannot fetch lnbits status manifest. {settings.lnbits_status_manifest}"
|
||||
)
|
||||
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||
|
||||
|
||||
async def register_watchdog():
|
||||
"""
|
||||
Registers a watchdog which will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
# TODO: implement watchdog porperly
|
||||
# logger.debug("Starting watchdog task")
|
||||
# global watchdog
|
||||
# watchdog = asyncio.create_task(watchdog_task())
|
||||
|
||||
|
||||
async def unregister_watchdog():
|
||||
"""
|
||||
Unregisters a watchdog task
|
||||
"""
|
||||
global watchdog
|
||||
if watchdog:
|
||||
logger.debug("Stopping watchdog task")
|
||||
watchdog.cancel()
|
||||
|
||||
|
||||
async def watchdog_task():
|
||||
while True:
|
||||
WALLET = get_wallet_class()
|
||||
if settings.lnbits_watchdog and WALLET.__class__.__name__ != "VoidWallet":
|
||||
try:
|
||||
delta, *_ = await get_balance_delta()
|
||||
logger.debug(f"Running watchdog task. current delta: {delta}")
|
||||
if delta + settings.lnbits_watchdog_delta <= 0:
|
||||
logger.error(f"Switching to VoidWallet. current delta: {delta}")
|
||||
await switch_to_voidwallet()
|
||||
except Exception as e:
|
||||
logger.error("Error in watchdog task", e)
|
||||
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
|
||||
|
||||
|
||||
async def register_task_listeners():
|
||||
"""
|
||||
@@ -38,8 +120,15 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
logger.trace("received invoice paid event")
|
||||
# send information to sse channel
|
||||
await dispatch_api_invoice_listeners(payment)
|
||||
await websocketUpdater(payment.wallet_id, payment.dict())
|
||||
|
||||
wallet = await get_wallet(payment.wallet_id)
|
||||
if wallet:
|
||||
await websocketUpdater(
|
||||
payment.wallet_id,
|
||||
{
|
||||
"wallet_balance": wallet.balance or None,
|
||||
"payment": payment.dict(),
|
||||
},
|
||||
)
|
||||
# dispatch webhook
|
||||
if payment.webhook and not payment.webhook_status:
|
||||
await dispatch_webhook(payment)
|
||||
|
||||
@@ -9,7 +9,18 @@
|
||||
<ul>
|
||||
{%raw%}
|
||||
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
|
||||
<li>Balance: {{balance / 1000}} sats</li>
|
||||
<li>
|
||||
Node Balance: {{(auditData.node_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
LNbits Balance: {{(auditData.lnbits_balance_msats /
|
||||
1000).toLocaleString()}} sats
|
||||
</li>
|
||||
<li>
|
||||
Reserve Percent: {{(auditData.node_balance_msats /
|
||||
auditData.lnbits_balance_msats * 100).toFixed(2)}} %
|
||||
</li>
|
||||
{%endraw%}
|
||||
</ul>
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<q-tab-panel name="security">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none" v-text="$t('security_tools')"></h6>
|
||||
<br />
|
||||
<div>
|
||||
<div class="row">
|
||||
<div v-if="serverlogEnabled" class="column" style="width: 100%">
|
||||
<div
|
||||
class="col bg-primary"
|
||||
style="padding-left: 5px; max-height: 20px; color: #fafafa"
|
||||
v-text="$t('server_logs')"
|
||||
></div>
|
||||
<div class="col" style="background-color: #292929">
|
||||
<q-scroll-area
|
||||
ref="logScroll"
|
||||
style="padding: 10px; color: #fafafa; height: 320px"
|
||||
>
|
||||
<small v-for="log in logs"
|
||||
>{% raw %}{{ log }}{% endraw %}<br
|
||||
/></small>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
@click="toggleServerLog"
|
||||
dense
|
||||
flat
|
||||
color="primary"
|
||||
:label="(serverlogEnabled) ? $t('disable_server_log') : $t('enable_server_log')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('ip_blocker')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formBlockedIPs"
|
||||
@keydown.enter="addBlockedIPs"
|
||||
type="text"
|
||||
:label="$t('enter_ip')"
|
||||
:hint="$t('block_access_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="addExtensionsManifest"
|
||||
dense
|
||||
flat
|
||||
icon="add"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="blocked_ip in formData.lnbits_blocked_ips"
|
||||
:key="blocked_ip"
|
||||
removable
|
||||
@remove="removeBlockedIPs(blocked_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ blocked_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formAllowedIPs"
|
||||
@keydown.enter="addAllowedIPs"
|
||||
type="text"
|
||||
:label="$t('enter_ip')"
|
||||
:hint="$t('allow_access_hint')"
|
||||
>
|
||||
<q-btn
|
||||
@click="addExtensionsManifest"
|
||||
dense
|
||||
flat
|
||||
icon="add"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="allowed_ip in formData.lnbits_allowed_ips"
|
||||
:key="allowed_ip"
|
||||
removable
|
||||
@remove="removeAllowedIPs(allowed_ip)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ allowed_ip }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('rate_limiter')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_rate_limit_no"
|
||||
:label="$t('number_of_requests')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-select
|
||||
filled
|
||||
:options="[$t('second'),$t('minute'),$t('hour')]"
|
||||
v-model="formData.lnbits_rate_limit_unit"
|
||||
:label="$t('time_unit')"
|
||||
></q-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_notifications')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_notifications_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notifications"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
<p
|
||||
v-if="!formData.lnbits_notifications"
|
||||
v-text="$t('notifications_disabled')"
|
||||
></p>
|
||||
<div v-if="formData.lnbits_notifications">
|
||||
{% include "admin/_tab_security_notifications.html" %}
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<p v-text="$t('notification_source')"></p>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_status_manifest"
|
||||
type="text"
|
||||
:label="$t('notification_source_label')"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p v-text="$t('killswitch')"></p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_killswitch')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_killswitch_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_killswitch"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('killswitch_interval')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('killswitch_interval_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_killswitch_interval"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_watchdog_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_watchdog"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_interval_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_interval"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_delta_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_delta"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
@@ -0,0 +1,70 @@
|
||||
{% raw %}
|
||||
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||
<q-icon size="28px" name="update"></q-icon>
|
||||
|
||||
<span v-text="$t('update_available', {version: statusData.version})"></span>
|
||||
<template v-slot:action>
|
||||
<a
|
||||
class="q-btn"
|
||||
color="white"
|
||||
target="_blank"
|
||||
href="https://github.com/lnbits/lnbits/releases"
|
||||
v-text="$t('releases')"
|
||||
></a>
|
||||
</template>
|
||||
</q-banner>
|
||||
<q-banner v-if="!updateAvailable" class="bg-green text-white">
|
||||
<q-icon size="28px" name="checknark"></q-icon>
|
||||
<span v-text="$t('latest_update', {version: statusData.version})"></span>
|
||||
</q-banner>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="statusData.notifications"
|
||||
:columns="statusDataTable.columns"
|
||||
:no-data-label="$t('no_notifications')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width> </q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props"
|
||||
>{{ col.label }}</q-th
|
||||
>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="props.row.type === 'update'"
|
||||
size="18px"
|
||||
name="update"
|
||||
color="green"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-if="props.row.type === 'warning'"
|
||||
size="18px"
|
||||
name="warning"
|
||||
color="red"
|
||||
></q-icon>
|
||||
</q-td>
|
||||
<q-td auto-width key="date" :props="props">
|
||||
{{ formatDate(props.row.date) }}
|
||||
</q-td>
|
||||
<q-td key="message" :props="props"
|
||||
>{{ props.row.message }}
|
||||
<a
|
||||
v-if="props.row.link"
|
||||
target="_blank"
|
||||
:href="props.row.link"
|
||||
v-text="$t('more')"
|
||||
></a
|
||||
></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endraw %}
|
||||
@@ -54,6 +54,20 @@
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Allowed currencies</p>
|
||||
<q-select
|
||||
filled
|
||||
v-model="formData.lnbits_allowed_currencies"
|
||||
multiple
|
||||
hint="Limit the number of available fiat currencies"
|
||||
label="Allowed currencies"
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<p>Admin Extensions</p>
|
||||
|
||||
@@ -88,6 +88,12 @@
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="security"
|
||||
:label="$t('security')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="theme"
|
||||
:label="$t('theme')"
|
||||
@@ -101,7 +107,8 @@
|
||||
<q-tab-panels v-model="tab" animated>
|
||||
{% include "admin/_tab_funding.html" %} {% include
|
||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %} {%
|
||||
include "admin/_tab_theme.html" %}
|
||||
include "admin/_tab_security.html" %} {% include
|
||||
"admin/_tab_theme.html" %}
|
||||
</q-tab-panels>
|
||||
</q-form>
|
||||
</q-card>
|
||||
@@ -164,6 +171,8 @@
|
||||
data: function () {
|
||||
return {
|
||||
settings: {},
|
||||
logs: [],
|
||||
serverlogEnabled: false,
|
||||
lnbits_theme_options: [
|
||||
'classic',
|
||||
'bitcoin',
|
||||
@@ -175,10 +184,30 @@
|
||||
'monochrome',
|
||||
'salvador'
|
||||
],
|
||||
auditData: {},
|
||||
statusData: {},
|
||||
statusDataTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: this.$t('date'),
|
||||
field: 'date'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'message'
|
||||
}
|
||||
]
|
||||
},
|
||||
formData: {},
|
||||
formAddAdmin: '',
|
||||
formAddUser: '',
|
||||
formAddExtensionsManifest: '',
|
||||
formAllowedIPs: '',
|
||||
formBlockedIPs: '',
|
||||
isSuperUser: false,
|
||||
wallet: {},
|
||||
cancel: {},
|
||||
@@ -349,11 +378,18 @@
|
||||
},
|
||||
created: function () {
|
||||
this.getSettings()
|
||||
this.getAudit()
|
||||
this.balance = +'{{ balance|safe }}'
|
||||
},
|
||||
computed: {
|
||||
lnbitsVersion() {
|
||||
return LNBITS_VERSION
|
||||
},
|
||||
checkChanges() {
|
||||
return !_.isEqual(this.settings, this.formData)
|
||||
},
|
||||
updateAvailable() {
|
||||
return LNBITS_VERSION !== this.statusData.version
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -364,7 +400,6 @@
|
||||
//admin_users = [...admin_users, addUser]
|
||||
this.formData.lnbits_admin_users = [...admin_users, addUser]
|
||||
this.formAddAdmin = ''
|
||||
//console.log(this.checkChanges)
|
||||
}
|
||||
},
|
||||
removeAdminUser(user) {
|
||||
@@ -406,6 +441,68 @@
|
||||
m => m !== manifest
|
||||
)
|
||||
},
|
||||
async toggleServerLog() {
|
||||
this.serverlogEnabled = !this.serverlogEnabled
|
||||
if (this.serverlogEnabled) {
|
||||
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
|
||||
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
|
||||
const localUrl =
|
||||
wsProto +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/api/v1/ws/' +
|
||||
digestHex
|
||||
this.ws = new WebSocket(localUrl)
|
||||
this.ws.addEventListener('message', async ({data}) => {
|
||||
this.logs.push(data.toString())
|
||||
const scrollArea = this.$refs.logScroll
|
||||
if (scrollArea) {
|
||||
const scrollTarget = scrollArea.getScrollTarget()
|
||||
const duration = 0
|
||||
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.ws.close()
|
||||
}
|
||||
},
|
||||
addAllowedIPs() {
|
||||
const allowedIPs = this.formAllowedIPs.trim()
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
if (
|
||||
allowedIPs &&
|
||||
allowedIPs.length &&
|
||||
!allowed_ips.includes(allowedIPs)
|
||||
) {
|
||||
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
|
||||
this.formAllowedIPs = ''
|
||||
}
|
||||
},
|
||||
removeAllowedIPs(allowed_ip) {
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
this.formData.lnbits_allowed_ips = allowed_ips.filter(
|
||||
a => a !== allowed_ip
|
||||
)
|
||||
},
|
||||
addBlockedIPs() {
|
||||
const blockedIPs = this.formBlockedIPs.trim()
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
if (
|
||||
blockedIPs &&
|
||||
blockedIPs.length &&
|
||||
!blocked_ips.includes(blockedIPs)
|
||||
) {
|
||||
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
|
||||
this.formBlockedIPs = ''
|
||||
}
|
||||
},
|
||||
removeBlockedIPs(blocked_ip) {
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
this.formData.lnbits_blocked_ips = blocked_ips.filter(
|
||||
b => b !== blocked_ip
|
||||
)
|
||||
},
|
||||
restartServer() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/restart/?usr=' + this.g.user.id)
|
||||
@@ -449,12 +546,43 @@
|
||||
this.settings.lnbits_allowed_funding_sources.map(f => {
|
||||
let opts = this.funding_sources.get(f)
|
||||
if (!opts) return
|
||||
|
||||
Object.keys(opts).forEach(e => {
|
||||
opts[e].value = this.settings[e]
|
||||
})
|
||||
})
|
||||
},
|
||||
formatDate(date) {
|
||||
return moment(date * 1000).fromNow()
|
||||
},
|
||||
getNotifications() {
|
||||
if (this.settings.lnbits_notifications) {
|
||||
axios
|
||||
.get(this.settings.lnbits_status_manifest)
|
||||
.then(response => {
|
||||
this.statusData = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
this.formData.lnbits_notifications = false
|
||||
error.response.data = {}
|
||||
error.response.data.message = 'Could not fetch status manifest.'
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
getAudit() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/admin/api/v1/audit/?usr=' + this.g.user.id,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.auditData = response.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getSettings() {
|
||||
LNbits.api
|
||||
.request(
|
||||
@@ -467,6 +595,7 @@
|
||||
this.settings = response.data
|
||||
this.formData = _.clone(this.settings)
|
||||
this.updateFundingData()
|
||||
this.getNotifications()
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
@@ -487,7 +616,9 @@
|
||||
.then(response => {
|
||||
this.needsRestart =
|
||||
this.settings.lnbits_backend_wallet_class !==
|
||||
this.formData.lnbits_backend_wallet_class
|
||||
this.formData.lnbits_backend_wallet_class ||
|
||||
this.settings.lnbits_killswitch !==
|
||||
this.formData.lnbits_killswitch
|
||||
this.settings = this.formData
|
||||
this.formData = _.clone(this.settings)
|
||||
this.updateFundingData()
|
||||
|
||||
@@ -108,7 +108,8 @@
|
||||
class="text-subtitle2 gt-sm"
|
||||
style="font-size: 11px; height: 34px"
|
||||
>
|
||||
{{ extension.shortDescription }}
|
||||
{{ extension.shortDescription ||
|
||||
extension.installedRelease?.description }}
|
||||
</div>
|
||||
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
|
||||
{{ extension.name }}
|
||||
@@ -184,7 +185,7 @@
|
||||
flat
|
||||
color="primary"
|
||||
type="a"
|
||||
:href="[extension.id, '?usr=', g.user.id].join('')"
|
||||
:href="[extension.id, '/?usr=', g.user.id].join('')"
|
||||
>{%raw%}{{ $t('open') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
@@ -198,8 +199,11 @@
|
||||
>
|
||||
{%raw%}{{ $t('disable') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-badge v-if="extension.isAdminOnly && !user.admin">
|
||||
{%raw%}{{ $t('admin_only') }}{%endraw%}
|
||||
</q-badge>
|
||||
<q-btn
|
||||
v-if="extension.isInstalled && !user.extensions.includes(extension.id) && extension.isActive"
|
||||
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
|
||||
flat
|
||||
color="primary"
|
||||
type="a"
|
||||
@@ -255,6 +259,17 @@
|
||||
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-checkbox
|
||||
v-model="uninstallAndDropDb"
|
||||
value="false"
|
||||
label="Cleanup database tables"
|
||||
>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
|
||||
{%raw%}{{ $t('extension_db_drop_info') }}{%endraw%}
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="uninstallExtension()"
|
||||
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
|
||||
@@ -266,6 +281,32 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showDropDbDialog">
|
||||
<q-card v-if="selectedExtension" class="q-pa-lg">
|
||||
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
|
||||
<p>{%raw%}{{ $t('extension_db_drop_warning') }}{%endraw%} <br /></p>
|
||||
<q-input
|
||||
v-model="dropDbExtensionId"
|
||||
:label="selectedExtension.id"
|
||||
></q-input>
|
||||
<br />
|
||||
<p>{%raw%}{{ $t('confirm_continue') }}{%endraw%}</p>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:disable="dropDbExtensionId !== selectedExtension.id"
|
||||
outline
|
||||
color="red"
|
||||
@click="dropExtensionDb()"
|
||||
>{%raw%}{{ $t('confirm') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showUpgradeDialog">
|
||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
@@ -300,6 +341,11 @@
|
||||
{%raw%}{{ $t('repository') }}{%endraw%}
|
||||
<br />
|
||||
<small v-text="repoName"></small>
|
||||
<q-tooltip
|
||||
><span
|
||||
v-text="selectedExtensionRepos[repoName].repo"
|
||||
></span
|
||||
></q-tooltip>
|
||||
</div>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
@@ -389,6 +435,13 @@
|
||||
>
|
||||
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else-if="selectedExtension?.hasDatabaseTables"
|
||||
@click="showDropDb()"
|
||||
flat
|
||||
color="red"
|
||||
:label="$t('drop_db')"
|
||||
></q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
|
||||
>
|
||||
@@ -407,8 +460,11 @@
|
||||
filteredExtensions: null,
|
||||
showUninstallDialog: false,
|
||||
showUpgradeDialog: false,
|
||||
showDropDbDialog: false,
|
||||
dropDbExtensionId: '',
|
||||
selectedExtension: null,
|
||||
selectedExtensionRepos: null,
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
user: null
|
||||
}
|
||||
@@ -497,6 +553,40 @@
|
||||
this.filteredExtensions = this.extensions.concat([])
|
||||
this.handleTabChanged('installed')
|
||||
this.tab = 'installed'
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Extension uninstalled!'
|
||||
})
|
||||
if (this.uninstallAndDropDb) {
|
||||
this.showDropDb()
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
extension.inProgress = false
|
||||
})
|
||||
},
|
||||
|
||||
dropExtensionDb: async function () {
|
||||
const extension = this.selectedExtension
|
||||
this.showUpgradeDialog = false
|
||||
this.showDropDbDialog = false
|
||||
this.dropDbExtensionId = ''
|
||||
extension.inProgress = true
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
`/api/v1/extension/${extension.id}/db?usr=${this.g.user.id}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
extension.installedRelease = null
|
||||
extension.inProgress = false
|
||||
extension.hasDatabaseTables = false
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Extension DB deleted!'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
@@ -525,6 +615,11 @@
|
||||
showUninstall: function () {
|
||||
this.showUpgradeDialog = false
|
||||
this.showUninstallDialog = true
|
||||
this.uninstallAndDropDb = false
|
||||
},
|
||||
|
||||
showDropDb: function () {
|
||||
this.showDropDbDialog = true
|
||||
},
|
||||
|
||||
showUpgrade: async function (extension) {
|
||||
@@ -541,7 +636,8 @@
|
||||
this.selectedExtensionRepos = data.reduce((repos, release) => {
|
||||
repos[release.source_repo] = repos[release.source_repo] || {
|
||||
releases: [],
|
||||
isInstalled: false
|
||||
isInstalled: false,
|
||||
repo: release.repo
|
||||
}
|
||||
release.inProgress = false
|
||||
release.error = null
|
||||
|
||||
@@ -150,6 +150,17 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="https://voltage.cloud">
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '/static/images/voltage.png' : '/static/images/voltagel.png'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col q-pl-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
</q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
key="memo"
|
||||
key="time"
|
||||
:props="props"
|
||||
style="white-space: normal; word-break: break-all"
|
||||
>
|
||||
@@ -192,11 +192,15 @@
|
||||
</a>
|
||||
</q-badge>
|
||||
{{ props.row.memo }}
|
||||
<br />
|
||||
|
||||
<i>
|
||||
{{ props.row.dateFrom }}<q-tooltip
|
||||
>{{ props.row.date }}</q-tooltip
|
||||
></i
|
||||
>
|
||||
</q-td>
|
||||
<q-td auto-width key="time" :props="props">
|
||||
<q-tooltip>{{ props.row.date }}</q-tooltip>
|
||||
{{ props.row.dateFrom }}
|
||||
</q-td>
|
||||
|
||||
{% endraw %}
|
||||
<q-td
|
||||
auto-width
|
||||
@@ -209,10 +213,8 @@
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width key="amount" v-else :props="props">
|
||||
{{ props.row.fsat }}
|
||||
</q-td>
|
||||
<q-td auto-width key="fee" :props="props">
|
||||
{{ props.row.fee }}
|
||||
{{ props.row.fsat }}<br />
|
||||
<i>fee {{ props.row.fee/1000 }}</i>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
@@ -552,7 +554,7 @@
|
||||
</h6>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p class="text-wrap">
|
||||
<strong v-text="$t('description')">:</strong> {{
|
||||
<strong v-text="$t('memo')">:</strong> {{
|
||||
parse.invoice.description }}<br />
|
||||
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
|
||||
<strong>Hash:</strong> {{ parse.invoice.hash }}
|
||||
@@ -786,10 +788,14 @@
|
||||
indicator-color="transparent"
|
||||
align="justify"
|
||||
>
|
||||
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
|
||||
<q-tab
|
||||
icon="file_download"
|
||||
@click="showReceiveDialog"
|
||||
:label="$t('receive')"
|
||||
>
|
||||
</q-tab>
|
||||
|
||||
<q-tab @click="showParseDialog" icon="file_upload" label="Send">
|
||||
<q-tab @click="showParseDialog" icon="file_upload" :label="$t('send')">
|
||||
</q-tab>
|
||||
<q-btn
|
||||
round
|
||||
|
||||
@@ -12,15 +12,35 @@ from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.services import update_cached_settings, update_wallet_balance
|
||||
from lnbits.core.services import (
|
||||
get_balance_delta,
|
||||
update_cached_settings,
|
||||
update_wallet_balance,
|
||||
)
|
||||
from lnbits.decorators import check_admin, check_super_user
|
||||
from lnbits.server import server_restart
|
||||
from lnbits.settings import AdminSettings, EditableSettings, settings
|
||||
|
||||
from .. import core_app
|
||||
from .. import core_app, core_app_extra
|
||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||
|
||||
|
||||
@core_app.get("/admin/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||
async def api_auditor():
|
||||
try:
|
||||
delta, node_balance, total_balance = await get_balance_delta()
|
||||
return {
|
||||
"delta_msats": int(delta),
|
||||
"node_balance_msats": int(node_balance),
|
||||
"lnbits_balance_msats": int(total_balance),
|
||||
}
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Could not audit balance.",
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/admin/api/v1/settings/")
|
||||
async def api_get_settings(
|
||||
user: User = Depends(check_admin),
|
||||
@@ -40,6 +60,7 @@ async def api_update_settings(
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
assert admin_settings, "Updated admin settings not found."
|
||||
update_cached_settings(admin_settings.dict())
|
||||
core_app_extra.register_new_ratelimiter()
|
||||
return {"status": "Success"}
|
||||
|
||||
|
||||
@@ -78,6 +99,11 @@ async def api_topup_balance(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="wallet does not exist."
|
||||
)
|
||||
|
||||
if settings.lnbits_backend_wallet_class == "VoidWallet":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="VoidWallet active"
|
||||
)
|
||||
|
||||
await update_wallet_balance(wallet_id=id, amount=int(amount))
|
||||
|
||||
return {"status": "Success"}
|
||||
|
||||
+36
-23
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
@@ -52,7 +51,7 @@ from lnbits.extension_manager import (
|
||||
get_valid_extensions,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi, url_for
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
fiat_amount_as_satoshis,
|
||||
@@ -63,15 +62,16 @@ from .. import core_app, core_app_extra, db
|
||||
from ..crud import (
|
||||
add_installed_extension,
|
||||
create_tinyurl,
|
||||
delete_dbversion,
|
||||
delete_installed_extension,
|
||||
delete_tinyurl,
|
||||
drop_extension_db,
|
||||
get_dbversions,
|
||||
get_payments,
|
||||
get_payments_paginated,
|
||||
get_standalone_payment,
|
||||
get_tinyurl,
|
||||
get_tinyurl_by_url,
|
||||
get_total_balance,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_wallet,
|
||||
@@ -670,6 +670,12 @@ async def api_perform_lnurlauth(
|
||||
|
||||
@core_app.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available():
|
||||
if len(settings.lnbits_allowed_currencies) > 0:
|
||||
return [
|
||||
item
|
||||
for item in currencies.keys()
|
||||
if item.upper() in settings.lnbits_allowed_currencies
|
||||
]
|
||||
return list(currencies.keys())
|
||||
|
||||
|
||||
@@ -718,25 +724,6 @@ async def img(request: Request, data):
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||
async def api_auditor():
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
error_message, node_balance = await WALLET.status()
|
||||
|
||||
if not error_message:
|
||||
delta = node_balance - total_balance
|
||||
else:
|
||||
node_balance, delta = 0, 0
|
||||
|
||||
return {
|
||||
"node_balance_msats": int(node_balance),
|
||||
"lnbits_balance_msats": int(total_balance),
|
||||
"delta_msats": int(delta),
|
||||
"timestamp": int(time.time()),
|
||||
}
|
||||
|
||||
|
||||
# UNIVERSAL WEBSOCKET MANAGER
|
||||
|
||||
|
||||
@@ -820,7 +807,7 @@ async def api_install_extension(
|
||||
ext_info.clean_extension_files()
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to install extension.",
|
||||
detail=f"Failed to install extension {ext_info.id} ({ext_info.installed_version}).",
|
||||
)
|
||||
|
||||
|
||||
@@ -902,6 +889,32 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||
)
|
||||
|
||||
|
||||
@core_app.delete(
|
||||
"/api/v1/extension/{ext_id}/db",
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def delete_extension_db(ext_id: str):
|
||||
try:
|
||||
db_version = (await get_dbversions()).get(ext_id, None)
|
||||
if not db_version:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Unknown extension id: {ext_id}",
|
||||
)
|
||||
await drop_extension_db(ext_id=ext_id)
|
||||
await delete_dbversion(ext_id=ext_id)
|
||||
logger.success(f"Database removed for extension '{ext_id}'")
|
||||
except HTTPException as ex:
|
||||
logger.error(ex)
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||
)
|
||||
|
||||
|
||||
# TINYURL
|
||||
|
||||
|
||||
|
||||
@@ -15,14 +15,17 @@ from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_admin, check_user_exists
|
||||
from lnbits.helpers import template_renderer, url_for
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
from ...extension_manager import InstallableExtension, get_valid_extensions
|
||||
from ...utils.exchange_rates import currencies
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
get_balance_check,
|
||||
get_dbversions,
|
||||
get_inactive_extensions,
|
||||
get_installed_extensions,
|
||||
get_user,
|
||||
@@ -113,6 +116,7 @@ async def extensions_install(
|
||||
|
||||
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
|
||||
inactive_extensions = await get_inactive_extensions()
|
||||
db_version = await get_dbversions()
|
||||
extensions = list(
|
||||
map(
|
||||
lambda ext: {
|
||||
@@ -124,7 +128,9 @@ async def extensions_install(
|
||||
"isFeatured": ext.featured,
|
||||
"dependencies": ext.dependencies,
|
||||
"isInstalled": ext.id in installed_exts_ids,
|
||||
"hasDatabaseTables": ext.id in db_version,
|
||||
"isAvailable": ext.id in all_extensions,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": dict(ext.latest_release)
|
||||
if ext.latest_release
|
||||
@@ -396,6 +402,7 @@ async def index(request: Request, user: User = Depends(check_admin)):
|
||||
"user": user.dict(),
|
||||
"settings": settings.dict(),
|
||||
"balance": balance,
|
||||
"currencies": list(currencies.keys()),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+37
-1
@@ -11,7 +11,7 @@ from sqlite3 import Row
|
||||
from typing import Any, Generic, List, Literal, Optional, Type, TypeVar
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel, ValidationError, root_validator
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy_aio.base import AsyncConnection
|
||||
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
|
||||
@@ -303,6 +303,21 @@ class Database(Compat):
|
||||
async def reuse_conn(self, conn: Connection):
|
||||
yield conn
|
||||
|
||||
@classmethod
|
||||
async def clean_ext_db_files(self, ext_id: str) -> bool:
|
||||
"""
|
||||
If the extension DB is stored directly on the filesystem (like SQLite) then delete the files and return True.
|
||||
Otherwise do nothing and return False.
|
||||
"""
|
||||
|
||||
if DB_TYPE == SQLITE:
|
||||
db_file = os.path.join(settings.lnbits_data_folder, f"ext_{ext_id}.sqlite3")
|
||||
if os.path.isfile(db_file):
|
||||
os.remove(db_file)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Operator(Enum):
|
||||
GT = "gt"
|
||||
@@ -344,6 +359,7 @@ class FromRowModel(BaseModel):
|
||||
|
||||
class FilterModel(BaseModel):
|
||||
__search_fields__: List[str] = []
|
||||
__sort_fields__: Optional[List[str]] = None
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -427,6 +443,14 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
|
||||
class Filters(BaseModel, Generic[TFilterModel]):
|
||||
"""
|
||||
Generic helper class for filtering and sorting data.
|
||||
For usage in an api endpoint, use the `parse_filters` dependency.
|
||||
|
||||
When constructing this class manually always make sure to pass a model so that the values can be validated.
|
||||
Otherwise, make sure to validate the inputs manually.
|
||||
"""
|
||||
|
||||
filters: List[Filter[TFilterModel]] = []
|
||||
search: Optional[str] = None
|
||||
|
||||
@@ -438,6 +462,18 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
model: Optional[Type[TFilterModel]] = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_sortby(cls, values):
|
||||
sortby = values.get("sortby")
|
||||
model = values.get("model")
|
||||
if sortby and model:
|
||||
model = values["model"]
|
||||
# if no sort fields are specified explicitly all fields are allowed
|
||||
allowed = model.__sort_fields__ or model.__fields__
|
||||
if sortby not in allowed:
|
||||
raise ValueError("Invalid sort field")
|
||||
return values
|
||||
|
||||
def pagination(self) -> str:
|
||||
stmt = ""
|
||||
if self.limit:
|
||||
|
||||
@@ -259,10 +259,6 @@ async def check_admin(usr: UUID4) -> User:
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
detail="User not authorized. No admin privileges.",
|
||||
)
|
||||
user.admin = True
|
||||
user.super_user = False
|
||||
if user.id == settings.super_user:
|
||||
user.super_user = True
|
||||
|
||||
return user
|
||||
|
||||
|
||||
+20
-11
@@ -25,6 +25,7 @@ class ExplicitRelease(BaseModel):
|
||||
archive: str
|
||||
hash: str
|
||||
dependencies: List[str] = []
|
||||
repo: Optional[str]
|
||||
icon: Optional[str]
|
||||
short_description: Optional[str]
|
||||
min_lnbits_version: Optional[str]
|
||||
@@ -98,18 +99,18 @@ async def fetch_github_repo_info(
|
||||
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
|
||||
repo_url = f"https://api.github.com/repos/{org}/{repository}"
|
||||
error_msg = "Cannot fetch extension repo"
|
||||
repo = await gihub_api_get(repo_url, error_msg)
|
||||
repo = await github_api_get(repo_url, error_msg)
|
||||
github_repo = GitHubRepo.parse_obj(repo)
|
||||
|
||||
lates_release_url = (
|
||||
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
|
||||
)
|
||||
error_msg = "Cannot fetch extension releases"
|
||||
latest_release: Any = await gihub_api_get(lates_release_url, error_msg)
|
||||
latest_release: Any = await github_api_get(lates_release_url, error_msg)
|
||||
|
||||
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
|
||||
error_msg = "Cannot fetch config for extension"
|
||||
config = await gihub_api_get(config_url, error_msg)
|
||||
config = await github_api_get(config_url, error_msg)
|
||||
|
||||
return (
|
||||
github_repo,
|
||||
@@ -120,14 +121,14 @@ async def fetch_github_repo_info(
|
||||
|
||||
async def fetch_manifest(url) -> Manifest:
|
||||
error_msg = "Cannot fetch extensions manifest"
|
||||
manifest = await gihub_api_get(url, error_msg)
|
||||
manifest = await github_api_get(url, error_msg)
|
||||
return Manifest.parse_obj(manifest)
|
||||
|
||||
|
||||
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
|
||||
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
|
||||
error_msg = "Cannot fetch extension releases"
|
||||
releases = await gihub_api_get(releases_url, error_msg)
|
||||
releases = await github_api_get(releases_url, error_msg)
|
||||
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
||||
|
||||
|
||||
@@ -138,11 +139,11 @@ async def fetch_github_release_config(
|
||||
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
||||
)
|
||||
error_msg = "Cannot fetch GitHub extension config"
|
||||
config = await gihub_api_get(config_url, error_msg)
|
||||
config = await github_api_get(config_url, error_msg)
|
||||
return ExtensionConfig.parse_obj(config)
|
||||
|
||||
|
||||
async def gihub_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = (
|
||||
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
|
||||
@@ -254,6 +255,7 @@ class ExtensionRelease(BaseModel):
|
||||
html_url: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
warning: Optional[str] = None
|
||||
repo: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
@@ -267,7 +269,7 @@ class ExtensionRelease(BaseModel):
|
||||
archive=r.zipball_url,
|
||||
source_repo=source_repo,
|
||||
is_github_release=True,
|
||||
# description=r.body, # bad for JSON
|
||||
repo=f"https://github.com/{source_repo}",
|
||||
html_url=r.html_url,
|
||||
)
|
||||
|
||||
@@ -286,6 +288,7 @@ class ExtensionRelease(BaseModel):
|
||||
is_version_compatible=e.is_version_compatible(),
|
||||
warning=e.warning,
|
||||
html_url=e.html_url,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
)
|
||||
|
||||
@@ -353,8 +356,14 @@ class InstallableExtension(BaseModel):
|
||||
return False
|
||||
return Path(self.ext_dir, "config.json").is_file()
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str:
|
||||
if self.installed_release:
|
||||
return self.installed_release.version
|
||||
return ""
|
||||
|
||||
def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name}.")
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
if ext_zip_file.is_file():
|
||||
os.remove(ext_zip_file)
|
||||
@@ -379,7 +388,7 @@ class InstallableExtension(BaseModel):
|
||||
)
|
||||
|
||||
def extract_archive(self):
|
||||
logger.info(f"Extracting extension {self.name}.")
|
||||
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
|
||||
Path("lnbits", "upgrades").mkdir(parents=True, exist_ok=True)
|
||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
|
||||
@@ -414,7 +423,7 @@ class InstallableExtension(BaseModel):
|
||||
Path(self.ext_upgrade_dir, self.id),
|
||||
Path(settings.lnbits_path, "extensions", self.id),
|
||||
)
|
||||
logger.success(f"Extension {self.name} installed.")
|
||||
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
|
||||
|
||||
def nofiy_upgrade(self) -> None:
|
||||
"""Update the list of upgraded extensions. The middleware will perform redirects based on this"""
|
||||
|
||||
@@ -2,9 +2,14 @@ from http import HTTPStatus
|
||||
from typing import Any, List, Tuple, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from lnbits.core import core_app_extra
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -189,3 +194,38 @@ class ExtensionsRedirectMiddleware:
|
||||
]
|
||||
|
||||
return "/" + "/".join(elements)
|
||||
|
||||
|
||||
def add_ratelimit_middleware(app: FastAPI):
|
||||
core_app_extra.register_new_ratelimiter()
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
|
||||
def add_ip_block_middleware(app: FastAPI):
|
||||
@app.middleware("http")
|
||||
async def block_allow_ip_middleware(request: Request, call_next):
|
||||
if not request.client:
|
||||
return JSONResponse(
|
||||
status_code=403, # Forbidden
|
||||
content={"detail": "No request client"},
|
||||
)
|
||||
if (
|
||||
request.client.host in settings.lnbits_blocked_ips
|
||||
and request.client.host not in settings.lnbits_allowed_ips
|
||||
):
|
||||
return JSONResponse(
|
||||
status_code=403, # Forbidden
|
||||
content={"detail": "IP is blocked"},
|
||||
)
|
||||
# this try: except: block is not needed on latest FastAPI (await call_next(request) is enough)
|
||||
# https://stackoverflow.com/questions/71222144/runtimeerror-no-response-returned-in-fastapi-when-refresh-request
|
||||
# TODO: remove after https://github.com/lnbits/lnbits/pull/1609 is merged
|
||||
try:
|
||||
return await call_next(request)
|
||||
except RuntimeError as exc:
|
||||
if str(exc) == "No response returned." and await request.is_disconnected():
|
||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||
raise # bubble up different exceptions
|
||||
|
||||
app.middleware("http")(block_allow_ip_middleware)
|
||||
|
||||
+24
-23
@@ -89,6 +89,7 @@ class ThemesSettings(LNbitsSettings):
|
||||
default="https://shop.lnbits.com/;/static/images/lnbits-shop-light.png;/static/images/lnbits-shop-dark.png"
|
||||
) # sneaky sneaky
|
||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||
lnbits_allowed_currencies: List[str] = Field(default=[])
|
||||
|
||||
|
||||
class OpsSettings(LNbitsSettings):
|
||||
@@ -100,6 +101,22 @@ class OpsSettings(LNbitsSettings):
|
||||
lnbits_denomination: str = Field(default="sats")
|
||||
|
||||
|
||||
class SecuritySettings(LNbitsSettings):
|
||||
lnbits_rate_limit_no: str = Field(default="200")
|
||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||
lnbits_allowed_ips: List[str] = Field(default=[])
|
||||
lnbits_blocked_ips: List[str] = Field(default=[])
|
||||
lnbits_notifications: bool = Field(default=False)
|
||||
lnbits_killswitch: bool = Field(default=False)
|
||||
lnbits_killswitch_interval: int = Field(default=60)
|
||||
lnbits_watchdog: bool = Field(default=False)
|
||||
lnbits_watchdog_interval: int = Field(default=60)
|
||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
||||
lnbits_status_manifest: str = Field(
|
||||
default="https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
|
||||
)
|
||||
|
||||
|
||||
class FakeWalletFundingSource(LNbitsSettings):
|
||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||
|
||||
@@ -206,6 +223,7 @@ class EditableSettings(
|
||||
ExtensionsSettings,
|
||||
ThemesSettings,
|
||||
OpsSettings,
|
||||
SecuritySettings,
|
||||
FundingSourcesSettings,
|
||||
BoltzExtensionSettings,
|
||||
LightningSettings,
|
||||
@@ -323,19 +341,6 @@ def set_cli_settings(**kwargs):
|
||||
setattr(settings, key, value)
|
||||
|
||||
|
||||
# set wallet class after settings are loaded
|
||||
def set_wallet_class(class_name: Optional[str] = None):
|
||||
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
||||
wallet_class = getattr(wallets_module, backend_wallet_class)
|
||||
global WALLET
|
||||
WALLET = wallet_class()
|
||||
|
||||
|
||||
def get_wallet_class():
|
||||
# wallet_class = getattr(wallets_module, settings.lnbits_backend_wallet_class)
|
||||
return WALLET
|
||||
|
||||
|
||||
def send_admin_user_to_saas():
|
||||
if settings.lnbits_saas_callback:
|
||||
with httpx.Client() as client:
|
||||
@@ -381,15 +386,11 @@ except:
|
||||
|
||||
settings.version = importlib.metadata.version("lnbits")
|
||||
|
||||
# printing environment variable for debugging
|
||||
if not settings.lnbits_admin_ui:
|
||||
logger.debug("Environment Settings:")
|
||||
for key, value in settings.dict(exclude_none=True).items():
|
||||
logger.debug(f"{key}: {value}")
|
||||
|
||||
def get_wallet_class():
|
||||
"""
|
||||
Backwards compatibility
|
||||
"""
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
wallets_module = importlib.import_module("lnbits.wallets")
|
||||
FAKE_WALLET = getattr(wallets_module, "FakeWallet")()
|
||||
|
||||
# initialize as fake wallet
|
||||
WALLET = FAKE_WALLET
|
||||
return get_wallet_class()
|
||||
|
||||
Vendored
+9
-9
File diff suppressed because one or more lines are too long
@@ -0,0 +1,90 @@
|
||||
window.localisation.br = {
|
||||
server: 'Servidor',
|
||||
theme: 'Tema',
|
||||
funding: 'Financiamento',
|
||||
users: 'Usuários',
|
||||
restart: 'Reiniciar servidor',
|
||||
save: 'Salvar',
|
||||
save_tooltip: 'Salvar suas alterações',
|
||||
topup: 'Recarregar',
|
||||
topup_wallet: 'Recarregar uma carteira',
|
||||
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
|
||||
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
|
||||
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
|
||||
reset_defaults: 'Redefinir para padrões',
|
||||
reset_defaults_tooltip:
|
||||
'Apagar todas as configurações e redefinir para os padrões.',
|
||||
download_backup: 'Fazer backup do banco de dados',
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
wallets: 'Carteiras',
|
||||
add_wallet: 'Adicionar nova carteira',
|
||||
delete_wallet: 'Excluir carteira',
|
||||
delete_wallet_desc:
|
||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||
rename_wallet: 'Renomear carteira',
|
||||
update_name: 'Atualizar nome',
|
||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
||||
donate: 'Doar',
|
||||
view_github: 'Ver no GitHub',
|
||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
commit_version: 'Versão de commit',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
credit_label: '%{denomination} para creditar',
|
||||
paste_request: 'Colar Pedido',
|
||||
create_invoice: 'Criar Fatura',
|
||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||
export_csv: 'Exportar para CSV',
|
||||
transactions: 'Transações',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendente',
|
||||
copy_invoice: 'Copiar fatura',
|
||||
close: 'Fechar',
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Ler',
|
||||
pay: 'Pagar',
|
||||
memo: 'Memo',
|
||||
date: 'Data',
|
||||
processing_payment: 'Processando pagamento...',
|
||||
not_enough_funds: 'Fundos insuficientes!',
|
||||
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
||||
invoice_waiting: 'Fatura aguardando pagamento',
|
||||
payment_received: 'Pagamento Recebido',
|
||||
payment_sent: 'Pagamento Enviado',
|
||||
receive: 'receber',
|
||||
send: 'enviar',
|
||||
outgoing_payment_pending: 'Pagamento pendente de saída',
|
||||
drain_funds: 'Drenar Fundos',
|
||||
drain_funds_desc:
|
||||
'Este é um código QR de retirada do LNURL para sugar tudo desta carteira. Não compartilhe com ninguém. É compatível com balanceCheck e balanceNotify para que sua carteira possa continuar retirando os fundos continuamente daqui após a primeira retirada.',
|
||||
i_understand: 'Eu entendo',
|
||||
copy_wallet_url: 'Copiar URL da carteira',
|
||||
disclaimer_dialog:
|
||||
'Funcionalidade de login a ser lançada em uma atualização futura, por enquanto, certifique-se de marcar esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
|
||||
no_transactions: 'Ainda não foram feitas transações',
|
||||
manage_extensions: 'Gerenciar extensões',
|
||||
manage_server: 'Gerenciar servidor',
|
||||
extensions: 'Extensões',
|
||||
no_extensions: 'Você não possui nenhuma extensão instalada :(',
|
||||
created: 'Criado',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
unit: 'Unidade',
|
||||
description: 'Descrição',
|
||||
expiry: 'Validade',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Comprovante de pagamento'
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
window.localisation.cn = {
|
||||
confirm: '确定',
|
||||
server: '服务器',
|
||||
theme: '主题',
|
||||
funding: '资金',
|
||||
users: '用户',
|
||||
restart: '重新启动服务器',
|
||||
save: '保存',
|
||||
save_tooltip: '保存更改',
|
||||
topup: '充值',
|
||||
topup_wallet: '给钱包充值',
|
||||
topup_hint: '使用钱包ID为任何钱包充值',
|
||||
restart_tooltip: '重新启动服务器以使更改生效',
|
||||
add_funds_tooltip: '为钱包添加资金',
|
||||
reset_defaults: '重置为默认设置',
|
||||
reset_defaults_tooltip: '删除所有设置并重置为默认设置',
|
||||
download_backup: '下载数据库备份',
|
||||
name_your_wallet: '给你的 %{name}钱包起个名字',
|
||||
paste_invoice_label: '粘贴发票,付款请求或lnurl*',
|
||||
lnbits_description:
|
||||
'LNbits 设置简单、轻量级,可以运行在任何闪电网络的版本上,目前支持 LND、Core Lightning、OpenNode、LNPay,甚至 LNbits 本身!您可以为自己运行 LNbits,或者为他人轻松提供资金托管。每个钱包都有自己的 API 密钥,你可以创建的钱包数量没有限制。能够把资金分开管理使 LNbits 成为一款有用的资金管理和开发工具。扩展程序增加了 LNbits 的额外功能,所以你可以在闪电网络上尝试各种尖端技术。我们已经尽可能简化了开发扩展程序的过程,作为一个免费和开源的项目,我们鼓励人们开发并提交自己的扩展程序。',
|
||||
export_to_phone: '通过二维码导出到手机',
|
||||
export_to_phone_desc:
|
||||
'这个二维码包含您钱包的URL。您可以使用手机扫描的方式打开您的钱包。',
|
||||
wallets: '钱包',
|
||||
add_wallet: '添加新钱包',
|
||||
delete_wallet: '删除钱包',
|
||||
delete_wallet_desc: '整个钱包将被删除,资金将无法恢复',
|
||||
rename_wallet: '重命名钱包',
|
||||
update_name: '更新名称',
|
||||
press_to_claim: '点击领取比特币',
|
||||
donate: '捐献',
|
||||
view_github: '在GitHub上查看',
|
||||
voidwallet_active: 'VoidWallet 已激活!付款功能已禁用。',
|
||||
use_with_caution: '请谨慎使用 - %{name}钱包还处于测试版阶段',
|
||||
toggle_darkmode: '切换暗黑模式',
|
||||
view_swagger_docs: '查看 LNbits Swagger API 文档',
|
||||
api_docs: 'API文档',
|
||||
commit_version: '提交版本',
|
||||
lnbits_version: 'LNbits版本',
|
||||
runs_on: '可运行在',
|
||||
credit_hint: '按 Enter 键充值账户',
|
||||
credit_label: '%{denomination} 充值',
|
||||
paste_request: '粘贴请求',
|
||||
create_invoice: '创建发票',
|
||||
camera_tooltip: '用相机扫描发票/二维码',
|
||||
export_csv: '导出为CSV',
|
||||
transactions: '交易记录',
|
||||
chart_tooltip: '显示图表',
|
||||
pending: '待处理',
|
||||
copy_invoice: '复制发票',
|
||||
close: '关闭',
|
||||
cancel: '取消',
|
||||
scan: '扫描',
|
||||
read: '读取',
|
||||
pay: '付款',
|
||||
memo: '备注',
|
||||
date: '日期',
|
||||
processing_payment: '正在处理支付...',
|
||||
not_enough_funds: '资金不足!',
|
||||
search_by_tag_memo_amount: '按标签、备注、金额搜索',
|
||||
invoice_waiting: '待支付的发票',
|
||||
payment_received: '收到付款',
|
||||
payment_sent: '付款已发送',
|
||||
receive: '收款',
|
||||
send: '付款',
|
||||
outgoing_payment_pending: '付款正在等待处理',
|
||||
drain_funds: '清空资金',
|
||||
drain_funds_desc:
|
||||
'这是一个 LNURL-取款的二维码,用于从该钱包中提取全部资金。请不要与他人分享。它与 balanceCheck 和 balanceNotify 兼容,因此在第一次取款后,您的钱包还可能会持续从这里提取资金',
|
||||
i_understand: '我明白',
|
||||
copy_wallet_url: '复制钱包URL',
|
||||
disclaimer_dialog:
|
||||
'登录功能将在以后的更新中发布,请将此页面加为书签,以便将来访问您的钱包!此服务处于测试阶段,我们不对资金的丢失承担任何责任。',
|
||||
no_transactions: '尚未进行任何交易',
|
||||
manage_server: '管理服务器',
|
||||
extensions: '扩展程序',
|
||||
no_extensions: '你没有安装任何扩展程序 :(',
|
||||
created: '已创建',
|
||||
|
||||
search_extensions: '搜索扩展程序',
|
||||
warning: '警告',
|
||||
manage: '管理',
|
||||
repository: '代码库',
|
||||
confirm_continue: '你确定要继续吗?',
|
||||
manage_extension_details: '安装/卸载扩展程序',
|
||||
install: '安装',
|
||||
uninstall: '卸载',
|
||||
drop_db: '删除数据',
|
||||
open: '打开',
|
||||
enable: '启用',
|
||||
enable_extension_details: '为当前用户启用扩展程序',
|
||||
disable: '禁用',
|
||||
installed: '已安装',
|
||||
activated: '已激活',
|
||||
deactivated: '已停用',
|
||||
release_notes: '发布说明',
|
||||
activate_extension_details: '对用户开放或禁用扩展程序',
|
||||
featured: '精选',
|
||||
all: '全部',
|
||||
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
||||
admin_only: '仅限管理员',
|
||||
new_version: '新版本',
|
||||
extension_depends_on: '依赖于:',
|
||||
extension_rating_soon: '即将推出评分',
|
||||
extension_installed_version: '已安装的版本',
|
||||
extension_uninstall_warning: '您即将对所有用户删除该扩展程序。',
|
||||
uninstall_confirm: '是的,卸载',
|
||||
extension_db_drop_info: '该扩展程序的所有数据将被永久删除。此操作无法撤销!',
|
||||
extension_db_drop_warning:
|
||||
'您即将删除该扩展的所有数据。请继续输入扩展程序名称以确认操作:',
|
||||
|
||||
extension_min_lnbits_version: '此版本要求最低的 LNbits 版本为',
|
||||
|
||||
payment_hash: '付款哈希',
|
||||
fee: '费',
|
||||
amount: '金额',
|
||||
unit: '单位',
|
||||
description: '详情',
|
||||
expiry: '过期时间',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: '付款证明'
|
||||
}
|
||||
@@ -14,16 +14,16 @@ window.localisation.de = {
|
||||
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
|
||||
reset_defaults: 'Zurücksetzen',
|
||||
reset_defaults_tooltip:
|
||||
'Alle Einstellungen zurücksetzen auf die Standardeinstellungen.',
|
||||
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
|
||||
download_backup: 'Datenbank-Backup herunterladen',
|
||||
name_your_wallet: 'Vergib deiner %{name} Wallet einen Namen',
|
||||
paste_invoice_label:
|
||||
'Füge eine Rechnung, Zahlungsanforderung oder lnurl ein *',
|
||||
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
|
||||
lnbits_description:
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, c-lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
|
||||
export_to_phone: 'Auf dem Telefon öffnen',
|
||||
export_to_phone_desc:
|
||||
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannste den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
|
||||
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
|
||||
wallets: 'Wallets',
|
||||
add_wallet: 'Wallet hinzufügen',
|
||||
delete_wallet: 'Wallet löschen',
|
||||
@@ -35,7 +35,8 @@ window.localisation.de = {
|
||||
donate: 'Spenden',
|
||||
view_github: 'Auf GitHub anzeigen',
|
||||
voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert',
|
||||
use_with_caution: 'BITTE MIT VORSICHT BENUTZEN- %{name} Wallet ist noch BETA',
|
||||
use_with_caution:
|
||||
'BITTE MIT VORSICHT BENUTZEN - %{name} Wallet ist noch BETA',
|
||||
toggle_darkmode: 'Auf Dark Mode umschalten',
|
||||
view_swagger_docs: 'LNbits Swagger API-Dokumente',
|
||||
api_docs: 'API docs',
|
||||
@@ -46,7 +47,7 @@ window.localisation.de = {
|
||||
paste_request: 'Anfrage einfügen',
|
||||
create_invoice: 'Rechnung erstellen',
|
||||
camera_tooltip:
|
||||
'Verwenden Sie die Kamera, um eine Rechnung oder einen QR-Code zu scannen',
|
||||
'Verwende die Kamera, um eine Rechnung oder einen QR-Code zu scannen',
|
||||
export_csv: 'Exportieren als CSV',
|
||||
transactions: 'Transaktionen',
|
||||
chart_tooltip: 'Diagramm anzeigen',
|
||||
@@ -65,10 +66,12 @@ window.localisation.de = {
|
||||
invoice_waiting: 'Rechnung wartend auf Zahlung',
|
||||
payment_received: 'Zahlung erhalten',
|
||||
payment_sent: 'Zahlung gesendet',
|
||||
receive: 'erhalten',
|
||||
send: 'schicken',
|
||||
outgoing_payment_pending: 'Ausgehende Zahlung wartend',
|
||||
drain_funds: 'Sats abziehen',
|
||||
drain_funds_desc:
|
||||
'LNURL-withdraw QR-Code, der das Abziehen aller Geldmittel aus dieser Wallet erlaubt. Teile ihn mit niemandem! Kompatibel mit balanceCheck und balanceNotify, so dass deine Brieftasche die Sats nach dem ersten Abzug kontinuierlich von hier abziehen kann.',
|
||||
'LNURL-withdraw QR-Code, der das Abziehen aller Geldmittel aus dieser Wallet erlaubt. Teile ihn mit niemandem! Kompatibel mit balanceCheck und balanceNotify, so dass dein Wallet die Sats nach dem ersten Abzug kontinuierlich von hier abziehen kann.',
|
||||
i_understand: 'Ich verstehe',
|
||||
copy_wallet_url: 'Wallet-URL kopieren',
|
||||
disclaimer_dialog:
|
||||
@@ -84,7 +87,7 @@ window.localisation.de = {
|
||||
warning: 'Warnung',
|
||||
manage: 'Verwalten',
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Sind Sie sicher, dass Sie fortfahren möchten?',
|
||||
confirm_continue: 'Bist du sicher, dass du fortfahren möchtest?',
|
||||
manage_extension_details: 'Erweiterung installieren/deinstallieren',
|
||||
install: 'Installieren',
|
||||
uninstall: 'Deinstallieren',
|
||||
@@ -104,7 +107,7 @@ window.localisation.de = {
|
||||
'(Nur Administratorkonten können Erweiterungen installieren)',
|
||||
new_version: 'Neue Version',
|
||||
extension_depends_on: 'Hängt ab von:',
|
||||
extension_rating_soon: 'Bewertungen kommen bald',
|
||||
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
||||
extension_installed_version: 'Installierte Version',
|
||||
extension_uninstall_warning:
|
||||
'Sie sind dabei, die Erweiterung für alle Benutzer zu entfernen.',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
window.localisation.en = {
|
||||
confirm: 'Yes',
|
||||
server: 'Server',
|
||||
theme: 'Theme',
|
||||
funding: 'Funding',
|
||||
@@ -17,7 +18,7 @@ window.localisation.en = {
|
||||
name_your_wallet: 'Name your %{name} wallet',
|
||||
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
|
||||
lnbits_description:
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||
@@ -62,6 +63,8 @@ window.localisation.en = {
|
||||
invoice_waiting: 'Invoice waiting to be paid',
|
||||
payment_received: 'Payment Received',
|
||||
payment_sent: 'Payment Sent',
|
||||
receive: 'receive',
|
||||
send: 'send',
|
||||
outgoing_payment_pending: 'Outgoing payment pending',
|
||||
drain_funds: 'Drain Funds',
|
||||
drain_funds_desc:
|
||||
@@ -84,6 +87,7 @@ window.localisation.en = {
|
||||
manage_extension_details: 'Install/uninstall extension',
|
||||
install: 'Install',
|
||||
uninstall: 'Uninstall',
|
||||
drop_db: 'Remove Data',
|
||||
open: 'Open',
|
||||
enable: 'Enable',
|
||||
enable_extension_details: 'Enable extension for current user',
|
||||
@@ -96,6 +100,7 @@ window.localisation.en = {
|
||||
featured: 'Featured',
|
||||
all: 'All',
|
||||
only_admins_can_install: '(Only admin accounts can install extensions)',
|
||||
admin_only: 'Admin Only',
|
||||
new_version: 'New Version',
|
||||
extension_depends_on: 'Depends on:',
|
||||
extension_rating_soon: 'Ratings coming soon',
|
||||
@@ -103,6 +108,11 @@ window.localisation.en = {
|
||||
extension_uninstall_warning:
|
||||
'You are about to remove the extension for all users.',
|
||||
uninstall_confirm: 'Yes, Uninstall',
|
||||
extension_db_drop_info:
|
||||
'All data for the extension will be permanently deleted. There is no way to undo this operation!',
|
||||
extension_db_drop_warning:
|
||||
'You are about to remove all data for the extension. Please type the extension name to continue:',
|
||||
|
||||
extension_min_lnbits_version: 'This release requires at least LNbits version',
|
||||
|
||||
payment_hash: 'Payment Hash',
|
||||
@@ -112,5 +122,52 @@ window.localisation.en = {
|
||||
description: 'Description',
|
||||
expiry: 'Expiry',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Payment Proof'
|
||||
payment_proof: 'Payment Proof',
|
||||
update_available: 'Update %{version} available!',
|
||||
latest_update: 'You are on the latest version %{version}.',
|
||||
notifications: 'Notifications',
|
||||
no_notifications: 'No notifications',
|
||||
notifications_disabled: 'LNbits status notifications are disabled.',
|
||||
enable_notifications: 'Enable Notifications',
|
||||
enable_notifications_desc:
|
||||
'If enabled it will fetch the latest LNbits Status updates, like security incidents and updates.',
|
||||
enable_killswitch: 'Enable Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.',
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
'How often the background task should check for the LNBits killswitch signal from the status source (in minutes).',
|
||||
enable_watchdog: 'Enable Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
|
||||
watchdog_interval: 'Watchdog Interval',
|
||||
watchdog_interval_desc:
|
||||
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
|
||||
watchdog_delta: 'Watchdog Delta',
|
||||
watchdog_delta_desc:
|
||||
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
|
||||
status: 'Status',
|
||||
notification_source: 'Notification Source',
|
||||
notification_source_label:
|
||||
'Source URL (only use the official LNbits status source, and sources you can trust)',
|
||||
more: 'more',
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blocker',
|
||||
security: 'Security',
|
||||
security_tools: 'Security tools',
|
||||
block_access_hint: 'Block access by IP',
|
||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
number_of_requests: 'Number of requests',
|
||||
time_unit: 'Time unit',
|
||||
minute: 'minute',
|
||||
second: 'second',
|
||||
hour: 'hour',
|
||||
disable_server_log: 'Disable Server Log',
|
||||
enable_server_log: 'Enable Server Log',
|
||||
coming_soon: 'Feature coming soon'
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ window.localisation.es = {
|
||||
name_your_wallet: 'Nombre de su billetera %{name}',
|
||||
paste_invoice_label: 'Pegue la factura aquí',
|
||||
lnbits_description:
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, c-lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
|
||||
export_to_phone: 'Exportar a teléfono con código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
|
||||
@@ -63,6 +63,8 @@ window.localisation.es = {
|
||||
invoice_waiting: 'Factura esperando pago',
|
||||
payment_received: 'Pago recibido',
|
||||
payment_sent: 'Pago enviado',
|
||||
receive: 'recibir',
|
||||
send: 'enviar',
|
||||
outgoing_payment_pending: 'Pago saliente pendiente',
|
||||
drain_funds: 'Drenar fondos',
|
||||
drain_funds_desc:
|
||||
|
||||
@@ -21,7 +21,7 @@ window.localisation.fr = {
|
||||
paste_invoice_label:
|
||||
'Coller une facture, une demande de paiement ou un code lnurl *',
|
||||
lnbits_description:
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, c-lightning, OpenNode, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
|
||||
export_to_phone: 'Exporter vers le téléphone avec un code QR',
|
||||
export_to_phone_desc:
|
||||
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
|
||||
@@ -67,6 +67,8 @@ window.localisation.fr = {
|
||||
invoice_waiting: 'Facture en attente de paiement',
|
||||
payment_received: 'Paiement reçu',
|
||||
payment_sent: 'Paiement envoyé',
|
||||
receive: 'recevoir',
|
||||
send: 'envoyer',
|
||||
outgoing_payment_pending: 'Paiement sortant en attente',
|
||||
drain_funds: 'Vider les fonds',
|
||||
drain_funds_desc:
|
||||
|
||||
@@ -19,7 +19,7 @@ window.localisation.it = {
|
||||
paste_invoice_label:
|
||||
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
|
||||
lnbits_description:
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, c-lightning, OpenNode, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
|
||||
export_to_phone: 'Esportazione su telefono con codice QR',
|
||||
export_to_phone_desc:
|
||||
"Questo codice QR contiene l'URL del portafoglio con accesso da amministratore. È possibile scansionarlo dal telefono per aprire il portafoglio da lì.",
|
||||
@@ -64,6 +64,8 @@ window.localisation.it = {
|
||||
invoice_waiting: 'Fattura in attesa di pagamento',
|
||||
payment_received: 'Pagamento ricevuto',
|
||||
payment_sent: 'Pagamento inviato',
|
||||
receive: 'ricevere',
|
||||
send: 'inviare',
|
||||
outgoing_payment_pending: 'Pagamento in uscita in attesa',
|
||||
drain_funds: 'Fondi di drenaggio',
|
||||
drain_funds_desc:
|
||||
|
||||
@@ -18,7 +18,7 @@ window.localisation.jp = {
|
||||
name_your_wallet: 'あなたのウォレットの名前 %{name}',
|
||||
paste_invoice_label: '請求書を貼り付けてください',
|
||||
lnbits_description:
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、c-lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
|
||||
export_to_phone: '電話にエクスポート',
|
||||
export_to_phone_desc:
|
||||
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
|
||||
@@ -62,6 +62,8 @@ window.localisation.jp = {
|
||||
invoice_waiting: '請求書を待っています',
|
||||
payment_received: 'お支払いありがとうございます',
|
||||
payment_sent: '支払いが完了しました',
|
||||
receive: '受け取る',
|
||||
send: '送信',
|
||||
outgoing_payment_pending: '支払い保留中',
|
||||
drain_funds: '資金を排出する',
|
||||
drain_funds_desc: 'ウォレットの残高をすべて他のウォレットに送金します',
|
||||
|
||||
@@ -19,7 +19,7 @@ window.localisation.nl = {
|
||||
name_your_wallet: 'Geef je %{name} portemonnee een naam',
|
||||
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
|
||||
lnbits_description:
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, c-lightning, OpenNode, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
|
||||
export_to_phone: 'Exporteren naar telefoon met QR-code',
|
||||
export_to_phone_desc:
|
||||
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
|
||||
@@ -65,6 +65,8 @@ window.localisation.nl = {
|
||||
invoice_waiting: 'Factuur wachtend op betaling',
|
||||
payment_received: 'Betaling ontvangen',
|
||||
payment_sent: 'Betaling verzonden',
|
||||
receive: 'ontvangen',
|
||||
send: 'versturen',
|
||||
voutgoing_payment_pending: 'Uitgaande betaling in behandeling',
|
||||
drain_funds: 'Geld opnemen',
|
||||
drain_funds_desc:
|
||||
|
||||
@@ -18,7 +18,7 @@ window.localisation.pi = {
|
||||
name_your_wallet: 'Name yer %{name} treasure chest',
|
||||
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
|
||||
lnbits_description:
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
|
||||
export_to_phone: 'Export to Phone with QR Code, me hearties',
|
||||
export_to_phone_desc:
|
||||
'This QR code contains yer chest URL with full access. Ye can scan it from yer phone to open yer chest from there, arr!',
|
||||
@@ -64,6 +64,8 @@ window.localisation.pi = {
|
||||
invoice_waiting: 'Invoice waiting to be plundered, arrr',
|
||||
payment_received: 'Payment Received like a treasure, argh',
|
||||
payment_sent: 'Payment Sent, hoist the colors! We´ve got some doubloons!',
|
||||
receive: 'booty',
|
||||
send: 'hoist',
|
||||
outgoing_payment_pending:
|
||||
'Outgoing payment pending in the port, ye scurvy dog',
|
||||
drain_funds: 'Plunder all the doubloons, ye buccaneer',
|
||||
|
||||
@@ -17,7 +17,7 @@ window.localisation.pl = {
|
||||
name_your_wallet: 'Nazwij swój portfel %{name}',
|
||||
paste_invoice_label: 'Wklej fakturę, żądanie zapłaty lub kod lnurl *',
|
||||
lnbits_description:
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, c-lightning, OpenNode, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
|
||||
export_to_phone: 'Eksport kodu QR na telefon',
|
||||
export_to_phone_desc:
|
||||
'Ten kod QR zawiera adres URL Twojego portfela z pełnym dostępem do niego. Możesz go zeskanować na swoim telefonie aby otworzyć na nim ten portfel.',
|
||||
@@ -62,6 +62,8 @@ window.localisation.pl = {
|
||||
invoice_waiting: 'Faktura oczekuje na zapłatę',
|
||||
payment_received: 'Otrzymano płatność',
|
||||
payment_sent: 'Wysłano płatność',
|
||||
receive: 'odbierać',
|
||||
send: 'wysłać',
|
||||
outgoing_payment_pending: 'Płatność wychodząca w toku',
|
||||
drain_funds: 'Opróżnij środki',
|
||||
drain_funds_desc:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
window.localisation.pt = {
|
||||
server: 'Servidor',
|
||||
theme: 'Tema',
|
||||
funding: 'Financiamento',
|
||||
users: 'Usuários',
|
||||
restart: 'Reiniciar servidor',
|
||||
save: 'Gravar',
|
||||
save_tooltip: 'Gravar as alterações',
|
||||
topup: 'Reforçar conta',
|
||||
topup_wallet: 'Recarregar uma carteira',
|
||||
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
|
||||
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
|
||||
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
|
||||
reset_defaults: 'Redefinir para padrões',
|
||||
reset_defaults_tooltip:
|
||||
'Apagar todas as configurações e redefinir para os padrões.',
|
||||
download_backup: 'Fazer backup da base de dados',
|
||||
name_your_wallet: 'Nomeie sua carteira %{name}',
|
||||
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
|
||||
lnbits_description:
|
||||
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
|
||||
export_to_phone: 'Exportar para o telefone com código QR',
|
||||
export_to_phone_desc:
|
||||
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
|
||||
wallets: 'Carteiras',
|
||||
add_wallet: 'Adicionar nova carteira',
|
||||
delete_wallet: 'Excluir carteira',
|
||||
delete_wallet_desc:
|
||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||
rename_wallet: 'Renomear carteira',
|
||||
update_name: 'Atualizar nome',
|
||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
||||
donate: 'Doar',
|
||||
view_github: 'Ver no GitHub',
|
||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
|
||||
toggle_darkmode: 'Alternar modo escuro',
|
||||
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
|
||||
api_docs: 'Documentação da API',
|
||||
commit_version: 'Versão de commit',
|
||||
lnbits_version: 'Versão do LNbits',
|
||||
runs_on: 'Executa em',
|
||||
credit_hint: 'Pressione Enter para creditar a conta',
|
||||
credit_label: '%{denomination} para creditar',
|
||||
paste_request: 'Colar Pedido',
|
||||
create_invoice: 'Criar Fatura',
|
||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||
export_csv: 'Exportar para CSV',
|
||||
transactions: 'Transações',
|
||||
chart_tooltip: 'Mostrar gráfico',
|
||||
pending: 'Pendente',
|
||||
copy_invoice: 'Copiar fatura',
|
||||
close: 'Fechar',
|
||||
cancel: 'Cancelar',
|
||||
scan: 'Escanear',
|
||||
read: 'Ler',
|
||||
pay: 'Pagar',
|
||||
memo: 'Memo',
|
||||
date: 'Data',
|
||||
processing_payment: 'Processando pagamento...',
|
||||
not_enough_funds: 'Fundos insuficientes!',
|
||||
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
||||
invoice_waiting: 'Fatura aguardando pagamento',
|
||||
payment_received: 'Pagamento Recebido',
|
||||
payment_sent: 'Pagamento Enviado',
|
||||
receive: 'receber',
|
||||
send: 'enviar',
|
||||
outgoing_payment_pending: 'Pagamento de saída pendente',
|
||||
drain_funds: 'Esvasiar carteira',
|
||||
drain_funds_desc:
|
||||
'Este é um código QR de saque LNURL para sacar tudo desta carteira. Não o partilhe com ninguém. É compatível com balanceCheck e balanceNotify para que a sua carteira possa continuar levantando os fundos continuamente daqui após o primeiro saque.',
|
||||
i_understand: 'Eu entendo',
|
||||
copy_wallet_url: 'Copiar URL da carteira',
|
||||
disclaimer_dialog:
|
||||
'Funcionalidade de login a ser lançada numa atualização futura, por enquanto, certifique-se que marca esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
|
||||
no_transactions: 'Ainda não foram feitas transações',
|
||||
manage_extensions: 'Gerir extensões',
|
||||
manage_server: 'Gerir servidor',
|
||||
extensions: 'Extensões',
|
||||
no_extensions: 'Não há nenhuma extensão instalada :(',
|
||||
created: 'Criado',
|
||||
payment_hash: 'Hash de pagamento',
|
||||
fee: 'Taxa',
|
||||
amount: 'Quantidade',
|
||||
unit: 'Unidade',
|
||||
description: 'Descrição',
|
||||
expiry: 'Validade',
|
||||
webhook: 'Webhook',
|
||||
payment_proof: 'Comprovativo de pagamento'
|
||||
}
|
||||
@@ -17,7 +17,7 @@ window.localisation.we = {
|
||||
name_your_wallet: 'Enwch eich waled %{name}',
|
||||
paste_invoice_label: 'Gludwch anfoneb, cais am daliad neu god lnurl *',
|
||||
lnbits_description:
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, c-lightning, OpenNode, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
|
||||
export_to_phone: 'Allforio i Ffôn gyda chod QR',
|
||||
export_to_phone_desc:
|
||||
'Mae`r cod QR hwn yn cynnwys URL eich waled gyda mynediad llawn. Gallwch ei sganio o`ch ffôn i agor eich waled oddi yno.',
|
||||
@@ -63,6 +63,8 @@ window.localisation.we = {
|
||||
invoice_waiting: 'Anfoneb yn aros i gael ei thalu',
|
||||
payment_received: 'Taliad a Dderbyniwyd',
|
||||
payment_sent: 'Taliad a Anfonwyd',
|
||||
receive: 'derbyn',
|
||||
send: 'anfon',
|
||||
outgoing_payment_pending: 'Taliad sy`n aros yn yr arfaeth',
|
||||
drain_funds: 'Cronfeydd Draenio',
|
||||
drain_funds_desc:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -241,6 +241,15 @@ window.LNbits = {
|
||||
}
|
||||
})
|
||||
},
|
||||
digestMessage: async function (message) {
|
||||
const msgUint8 = new TextEncoder().encode(message)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return hashHex
|
||||
},
|
||||
formatCurrency: function (value, currency) {
|
||||
return new Intl.NumberFormat(window.LOCALE, {
|
||||
style: 'currency',
|
||||
@@ -343,12 +352,16 @@ window.windowMixin = {
|
||||
user: null,
|
||||
wallet: null,
|
||||
payments: [],
|
||||
allowedThemes: null
|
||||
allowedThemes: null,
|
||||
langs: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
activeLanguage: function (lang) {
|
||||
return window.i18n.locale === lang
|
||||
},
|
||||
changeLanguage: function (newValue) {
|
||||
window.i18n.locale = newValue
|
||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||
@@ -388,6 +401,8 @@ window.windowMixin = {
|
||||
window.i18n.locale = locale
|
||||
}
|
||||
|
||||
this.g.langs = window.langs ?? []
|
||||
|
||||
addEventListener('offline', event => {
|
||||
this.g.offline = true
|
||||
})
|
||||
|
||||
@@ -213,71 +213,70 @@ Vue.component('lnbits-payment-details', {
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="q-py-md" style="text-align: left">
|
||||
<div class="row justify-center q-mb-md">
|
||||
<q-badge v-if="hasTag" color="yellow" text-color="black">
|
||||
#{{ payment.tag }}
|
||||
</q-badge>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('created')"></b>:</div>
|
||||
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('expiry')"></b>:</div>
|
||||
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('description')"></b>:</div>
|
||||
<div class="col-9">{{ payment.memo }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('amount')"></b>:</div>
|
||||
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('fee')"></b>:</div>
|
||||
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ payment.payment_hash }}
|
||||
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="payment.webhook">
|
||||
<div class="col-3"><b v-text="$t('webhook')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
{{ payment.webhook }}
|
||||
<q-badge :color="webhookStatusColor" text-color="white">
|
||||
{{ webhookStatusText }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="hasPreimage">
|
||||
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
|
||||
</div>
|
||||
<div class="row" v-for="entry in extras">
|
||||
<div class="col-3">
|
||||
<q-badge v-if="hasTag" color="secondary" text-color="white">
|
||||
extra
|
||||
</q-badge>
|
||||
<b>{{ entry.key }}</b>:
|
||||
</div>
|
||||
<div class="col-9 text-wrap mono">{{ entry.value }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="hasSuccessAction">
|
||||
<div class="col-3"><b>Success action</b>:</div>
|
||||
<div class="col-9">
|
||||
<lnbits-lnurlpay-success-action
|
||||
:payment="payment"
|
||||
:success_action="payment.extra.success_action"
|
||||
></lnbits-lnurlpay-success-action>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-py-md" style="text-align: left">
|
||||
|
||||
<div v-if="payment.tag" class="row justify-center q-mb-md">
|
||||
<q-badge v-if="hasTag" color="yellow" text-color="black">
|
||||
#{{ payment.tag }}
|
||||
</q-badge>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<b v-text="$t('created')"></b>:
|
||||
{{ payment.date }} ({{ payment.dateFrom }})
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<b v-text="$t('expiry')"></b>:
|
||||
{{ payment.expirydate }} ({{ payment.expirydateFrom }})
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<b v-text="$t('amount')"></b>:
|
||||
{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<b v-text="$t('fee')"></b>:
|
||||
{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
|
||||
</div>
|
||||
|
||||
<div class="text-wrap">
|
||||
<b style="white-space: nowrap;" v-text="$t('payment_hash')"></b>: {{ payment.payment_hash }}
|
||||
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div class="text-wrap">
|
||||
<b style="white-space: nowrap;" v-text="$t('memo')"></b>: {{ payment.memo }}
|
||||
</div>
|
||||
|
||||
<div class="text-wrap" v-if="payment.webhook">
|
||||
<b style="white-space: nowrap;" v-text="$t('webhook')"></b>: {{ payment.webhook }}: <q-badge :color="webhookStatusColor" text-color="white">
|
||||
{{ webhookStatusText }}
|
||||
</q-badge>
|
||||
</div>
|
||||
|
||||
<div class="text-wrap" v-if="hasPreimage">
|
||||
<b style="white-space: nowrap;" v-text="$t('payment_proof')"></b>: {{ payment.preimage }}
|
||||
</div>
|
||||
|
||||
<div class="row" v-for="entry in extras">
|
||||
<q-badge v-if="hasTag" color="secondary" text-color="white">
|
||||
extra
|
||||
</q-badge>
|
||||
<b>{{ entry.key }}</b>:
|
||||
{{ entry.value }}
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="hasSuccessAction">
|
||||
<b>Success action</b>:
|
||||
<lnbits-lnurlpay-success-action
|
||||
:payment="payment"
|
||||
:success_action="payment.extra.success_action"
|
||||
></lnbits-lnurlpay-success-action>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
computed: {
|
||||
hasPreimage() {
|
||||
|
||||
@@ -19,12 +19,15 @@
|
||||
"/static/i18n/fr.js",
|
||||
"/static/i18n/it.js",
|
||||
"/static/i18n/jp.js",
|
||||
"/static/i18n/cn.js",
|
||||
"/static/i18n/nl.js",
|
||||
"/static/i18n/pi.js",
|
||||
"/static/i18n/pl.js",
|
||||
"/static/i18n/fr.js",
|
||||
"/static/i18n/nl.js",
|
||||
"/static/i18n/we.js",
|
||||
"/static/i18n/pt.js",
|
||||
"/static/i18n/br.js",
|
||||
"/static/js/base.js",
|
||||
"/static/js/components.js",
|
||||
"/static/js/bolt11-decoder.js"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ from lnbits.core.crud import (
|
||||
get_standalone_payment,
|
||||
)
|
||||
from lnbits.core.services import redeem_lnurl_withdraw
|
||||
from lnbits.settings import get_wallet_class
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
from .core import db
|
||||
|
||||
|
||||
+32
-51
@@ -78,55 +78,19 @@
|
||||
icon="language"
|
||||
class="q-pl-md"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('en')">
|
||||
<q-item-section>
|
||||
<q-item-label>EN</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('de')">
|
||||
<q-item-section>
|
||||
<q-item-label>DE</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('es')">
|
||||
<q-item-section>
|
||||
<q-item-label>ES</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('jp')">
|
||||
<q-item-section>
|
||||
<q-item-label>JP</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('fr')">
|
||||
<q-item-section>
|
||||
<q-item-label>FR</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('we')">
|
||||
<q-item-section>
|
||||
<q-item-label>WE</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('it')">
|
||||
<q-item-section>
|
||||
<q-item-label>IT</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('pi')">
|
||||
<q-item-section>
|
||||
<q-item-label>PI</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('pl')">
|
||||
<q-item-section>
|
||||
<q-item-label>PL</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable v-close-popup @click="changeLanguage('nl')">
|
||||
<q-item-section>
|
||||
<q-item-label>NL</q-item-label>
|
||||
<q-list v-for="(lang, index) in g.langs" :key="index">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
:active="activeLanguage(lang.value)"
|
||||
@click="changeLanguage(lang.value)"
|
||||
><q-item-section>
|
||||
{% raw %}
|
||||
<q-item-label
|
||||
>{{lang.display ?? lang.value.toUpperCase()}}</q-item-label
|
||||
>
|
||||
<q-tooltip>{{lang.label}}</q-tooltip>
|
||||
{% endraw %}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@@ -275,6 +239,7 @@
|
||||
class="bg-transparent q-px-lg q-py-md"
|
||||
:class="{'text-dark': !$q.dark.isActive}"
|
||||
>
|
||||
<q-space class="q-py-lg lt-md"></q-space>
|
||||
<q-toolbar class="gt-sm">
|
||||
<q-toolbar-title class="text-caption">
|
||||
{{ SITE_TITLE }}, {{SITE_TAGLINE}}
|
||||
@@ -328,11 +293,27 @@
|
||||
{% endfor %}
|
||||
<!---->
|
||||
<script type="text/javascript">
|
||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
|
||||
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
|
||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }}
|
||||
const LNBITS_VERSION = {{ LNBITS_VERSION | tojson }}
|
||||
if (themes && themes.length) {
|
||||
window.allowedThemes = themes.map(str => str.trim())
|
||||
}
|
||||
window.langs = [
|
||||
{value: 'en', label: 'English', display: 'EN'},
|
||||
{value: 'de', label: 'Deutsch', display: 'DE'},
|
||||
{value: 'es', label: 'Español', display: 'ES'},
|
||||
{value: 'jp', label: '日本語', display: 'JP'},
|
||||
{value: 'cn', label: '中文', display: 'CN'},
|
||||
{value: 'fr', label: 'Français', display: 'FR'},
|
||||
{value: 'it', label: 'Italiano', display: 'IT'},
|
||||
{value: 'pi', label: 'Pirate', display: 'PI'},
|
||||
{value: 'nl', label: 'Nederlands', display: 'NL'},
|
||||
{value: 'we', label: 'Cymraeg', display: 'CY'},
|
||||
{value: 'pl', label: 'Polski', display: 'PL'},
|
||||
{value: 'pt', label: 'Português', display: 'PT'},
|
||||
{value: 'br', label: 'Português', display: 'BR'}
|
||||
]
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# flake8: noqa: F401
|
||||
import importlib
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets.base import Wallet
|
||||
|
||||
from .cliche import ClicheWallet
|
||||
from .cln import CoreLightningWallet
|
||||
from .cln import CoreLightningWallet as CLightningWallet
|
||||
@@ -12,3 +20,21 @@ from .lntips import LnTipsWallet
|
||||
from .opennode import OpenNodeWallet
|
||||
from .spark import SparkWallet
|
||||
from .void import VoidWallet
|
||||
|
||||
|
||||
def set_wallet_class(class_name: Optional[str] = None):
|
||||
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
||||
wallet_class = getattr(wallets_module, backend_wallet_class)
|
||||
global WALLET
|
||||
WALLET = wallet_class()
|
||||
|
||||
|
||||
def get_wallet_class() -> Wallet:
|
||||
return WALLET
|
||||
|
||||
|
||||
wallets_module = importlib.import_module("lnbits.wallets")
|
||||
FAKE_WALLET: Wallet = FakeWallet()
|
||||
|
||||
# initialize as fake wallet
|
||||
WALLET: Wallet = FAKE_WALLET
|
||||
|
||||
@@ -48,6 +48,9 @@ class PaymentStatus(NamedTuple):
|
||||
|
||||
|
||||
class Wallet(ABC):
|
||||
async def cleanup(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def status(self) -> Coroutine[None, None, StatusResponse]:
|
||||
pass
|
||||
@@ -58,6 +61,7 @@ class Wallet(ABC):
|
||||
amount: int,
|
||||
memo: Optional[str] = None,
|
||||
description_hash: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> Coroutine[None, None, InvoiceResponse]:
|
||||
pass
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class CoreLightningWallet(Wallet):
|
||||
try:
|
||||
funds = self.ln.listfunds()
|
||||
return StatusResponse(
|
||||
None, sum([ch["channel_sat"] * 1000 for ch in funds["channels"]])
|
||||
None, sum([int(ch["our_amount_msat"]) for ch in funds["channels"]])
|
||||
)
|
||||
except RpcError as exc:
|
||||
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
|
||||
@@ -141,7 +141,7 @@ class CoreLightningWallet(Wallet):
|
||||
except Exception as exc:
|
||||
return PaymentResponse(False, None, None, None, str(exc))
|
||||
|
||||
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
|
||||
fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"])
|
||||
return PaymentResponse(
|
||||
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
|
||||
)
|
||||
|
||||
+30
-40
@@ -3,7 +3,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import urllib.parse
|
||||
from typing import AsyncGenerator, Dict, Optional
|
||||
from typing import Any, AsyncGenerator, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -41,12 +41,13 @@ class EclairWallet(Wallet):
|
||||
encodedAuth = base64.b64encode(f":{passw}".encode())
|
||||
auth = str(encodedAuth, "utf-8")
|
||||
self.auth = {"Authorization": f"Basic {auth}"}
|
||||
self.client = httpx.AsyncClient(base_url=self.url, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/globalbalance", headers=self.auth, timeout=5
|
||||
)
|
||||
r = await self.client.post("/globalbalance", timeout=5)
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
@@ -69,24 +70,21 @@ class EclairWallet(Wallet):
|
||||
unhashed_description: Optional[bytes] = None,
|
||||
**kwargs,
|
||||
) -> InvoiceResponse:
|
||||
|
||||
data: Dict = {
|
||||
data: Dict[str, Any] = {
|
||||
"amountMsat": amount * 1000,
|
||||
"description_hash": b"",
|
||||
"description": memo,
|
||||
}
|
||||
if kwargs.get("expiry"):
|
||||
data["expireIn"] = kwargs["expiry"]
|
||||
|
||||
# Either 'description' (string) or 'descriptionHash' must be supplied
|
||||
if description_hash:
|
||||
data["descriptionHash"] = description_hash.hex()
|
||||
elif unhashed_description:
|
||||
data["descriptionHash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||
else:
|
||||
data["description"] = memo
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/createinvoice", headers=self.auth, data=data, timeout=40
|
||||
)
|
||||
r = await self.client.post("/createinvoice", data=data, timeout=40)
|
||||
|
||||
if r.is_error:
|
||||
try:
|
||||
@@ -101,13 +99,11 @@ class EclairWallet(Wallet):
|
||||
return InvoiceResponse(True, data["paymentHash"], data["serialized"], None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/payinvoice",
|
||||
headers=self.auth,
|
||||
data={"invoice": bolt11, "blocking": True},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/payinvoice",
|
||||
data={"invoice": bolt11, "blocking": True},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
@@ -127,13 +123,11 @@ class EclairWallet(Wallet):
|
||||
|
||||
# We do all this again to get the fee:
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getsentinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getsentinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if "error" in r.json():
|
||||
try:
|
||||
@@ -161,12 +155,10 @@ class EclairWallet(Wallet):
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getreceivedinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getreceivedinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
@@ -185,13 +177,11 @@ class EclairWallet(Wallet):
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.url}/getsentinfo",
|
||||
headers=self.auth,
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/getsentinfo",
|
||||
data={"paymentHash": checking_id},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
|
||||
+20
-35
@@ -29,17 +29,18 @@ class LNbitsWallet(Wallet):
|
||||
if not self.endpoint or not key:
|
||||
raise Exception("cannot initialize lnbits wallet")
|
||||
self.key = {"X-Api-Key": key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.key)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/wallet", headers=self.key, timeout=15
|
||||
)
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||
)
|
||||
try:
|
||||
r = await self.client.get(url="/api/v1/wallet", timeout=15)
|
||||
except Exception as exc:
|
||||
return StatusResponse(
|
||||
f"Failed to connect to {self.endpoint} due to: {exc}", 0
|
||||
)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
@@ -69,10 +70,7 @@ class LNbitsWallet(Wallet):
|
||||
if unhashed_description:
|
||||
data["unhashed_description"] = unhashed_description.hex()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments", headers=self.key, json=data
|
||||
)
|
||||
r = await self.client.post(url="/api/v1/payments", json=data)
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
not r.is_error,
|
||||
None,
|
||||
@@ -89,20 +87,12 @@ class LNbitsWallet(Wallet):
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/payments",
|
||||
headers=self.key,
|
||||
json={"out": True, "bolt11": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
ok, checking_id, _, _, error_message = (
|
||||
not r.is_error,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
r = await self.client.post(
|
||||
url="/api/v1/payments",
|
||||
json={"out": True, "bolt11": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
ok = not r.is_error
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["detail"]
|
||||
@@ -118,11 +108,9 @@ class LNbitsWallet(Wallet):
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}",
|
||||
headers=self.key,
|
||||
)
|
||||
r = await self.client.get(
|
||||
url=f"/api/v1/payments/{checking_id}",
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
return PaymentStatus(r.json()["paid"])
|
||||
@@ -130,10 +118,7 @@ class LNbitsWallet(Wallet):
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.key
|
||||
)
|
||||
r = await self.client.get(url=f"/api/v1/payments/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
+51
-62
@@ -64,14 +64,17 @@ class LndRestWallet(Wallet):
|
||||
self.cert = cert or True
|
||||
|
||||
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
|
||||
self.client = httpx.AsyncClient(
|
||||
base_url=self.endpoint, headers=self.auth, verify=self.cert
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/balance/channels", headers=self.auth
|
||||
)
|
||||
r.raise_for_status()
|
||||
r = await self.client.get("/v1/balance/channels")
|
||||
r.raise_for_status()
|
||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||
return StatusResponse(f"Unable to connect to {self.endpoint}. {exc}", 0)
|
||||
|
||||
@@ -104,10 +107,7 @@ class LndRestWallet(Wallet):
|
||||
hashlib.sha256(unhashed_description).digest()
|
||||
).decode("ascii")
|
||||
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/v1/invoices", headers=self.auth, json=data
|
||||
)
|
||||
r = await self.client.post(url="/v1/invoices", json=data)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.text
|
||||
@@ -125,17 +125,15 @@ class LndRestWallet(Wallet):
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
# set the fee limit for the payment
|
||||
lnrpcFeeLimit = dict()
|
||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||
# set the fee limit for the payment
|
||||
lnrpcFeeLimit = dict()
|
||||
lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}"
|
||||
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/v1/channels/transactions",
|
||||
headers=self.auth,
|
||||
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
url="/v1/channels/transactions",
|
||||
json={"payment_request": bolt11, "fee_limit": lnrpcFeeLimit},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if r.is_error or r.json().get("payment_error"):
|
||||
error_message = r.json().get("payment_error") or r.text
|
||||
@@ -148,10 +146,7 @@ class LndRestWallet(Wallet):
|
||||
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient(verify=self.cert) as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(url=f"/v1/invoice/{checking_id}")
|
||||
|
||||
if r.is_error or not r.json().get("settled"):
|
||||
# this must also work when checking_id is not a hex recognizable by lnd
|
||||
@@ -172,7 +167,7 @@ class LndRestWallet(Wallet):
|
||||
except ValueError:
|
||||
return PaymentStatus(None)
|
||||
|
||||
url = f"{self.endpoint}/v2/router/track/{checking_id}"
|
||||
url = f"/v2/router/track/{checking_id}"
|
||||
|
||||
# check payment.status:
|
||||
# https://api.lightning.community/?python=#paymentpaymentstatus
|
||||
@@ -183,52 +178,46 @@ class LndRestWallet(Wallet):
|
||||
"FAILED": False,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.auth, verify=self.cert
|
||||
) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for json_line in r.aiter_lines():
|
||||
try:
|
||||
line = json.loads(json_line)
|
||||
if line.get("error"):
|
||||
logger.error(
|
||||
line["error"]["message"]
|
||||
if "message" in line["error"]
|
||||
else line["error"]
|
||||
)
|
||||
return PaymentStatus(None)
|
||||
payment = line.get("result")
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
except:
|
||||
continue
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for json_line in r.aiter_lines():
|
||||
try:
|
||||
line = json.loads(json_line)
|
||||
if line.get("error"):
|
||||
logger.error(
|
||||
line["error"]["message"]
|
||||
if "message" in line["error"]
|
||||
else line["error"]
|
||||
)
|
||||
return PaymentStatus(None)
|
||||
payment = line.get("result")
|
||||
if payment is not None and payment.get("status"):
|
||||
return PaymentStatus(
|
||||
paid=statuses[payment["status"]],
|
||||
fee_msat=payment.get("fee_msat"),
|
||||
preimage=payment.get("payment_preimage"),
|
||||
)
|
||||
else:
|
||||
return PaymentStatus(None)
|
||||
except:
|
||||
continue
|
||||
|
||||
return PaymentStatus(None)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while True:
|
||||
try:
|
||||
url = self.endpoint + "/v1/invoices/subscribe"
|
||||
async with httpx.AsyncClient(
|
||||
timeout=None, headers=self.auth, verify=self.cert
|
||||
) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
continue
|
||||
except:
|
||||
url = "/v1/invoices/subscribe"
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
inv = json.loads(line)["result"]
|
||||
if not inv["settled"]:
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
payment_hash = base64.b64decode(inv["r_hash"]).hex()
|
||||
yield payment_hash
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f"lost connection to lnd invoices stream: '{exc}', retrying in 5 seconds"
|
||||
|
||||
+23
-29
@@ -32,12 +32,15 @@ class LNPayWallet(Wallet):
|
||||
self.wallet_key = wallet_key
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"X-Api-Key": settings.lnpay_api_key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
url = f"{self.endpoint}/wallet/{self.wallet_key}"
|
||||
url = f"/wallet/{self.wallet_key}"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(url, headers=self.auth, timeout=60)
|
||||
r = await self.client.get(url, timeout=60)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return StatusResponse(f"Unable to connect to '{url}'", 0)
|
||||
|
||||
@@ -69,13 +72,11 @@ class LNPayWallet(Wallet):
|
||||
else:
|
||||
data["memo"] = memo or ""
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/wallet/{self.wallet_key}/invoice",
|
||||
headers=self.auth,
|
||||
json=data,
|
||||
timeout=60,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/invoice",
|
||||
json=data,
|
||||
timeout=60,
|
||||
)
|
||||
ok, checking_id, payment_request, error_message = (
|
||||
r.status_code == 201,
|
||||
None,
|
||||
@@ -90,13 +91,11 @@ class LNPayWallet(Wallet):
|
||||
return InvoiceResponse(ok, checking_id, payment_request, error_message)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
|
||||
headers=self.auth,
|
||||
json={"payment_request": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/wallet/{self.wallet_key}/withdraw",
|
||||
json={"payment_request": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
try:
|
||||
data = r.json()
|
||||
@@ -117,11 +116,9 @@ class LNPayWallet(Wallet):
|
||||
return await self.get_payment_status(checking_id)
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
url=f"{self.endpoint}/lntx/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.get(
|
||||
url=f"/lntx/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
@@ -155,12 +152,9 @@ class LNPayWallet(Wallet):
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
lntx_id = data["data"]["wtx"]["lnTx"]["id"]
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/lntx/{lntx_id}?fields=settled", headers=self.auth
|
||||
)
|
||||
data = r.json()
|
||||
if data["settled"]:
|
||||
await self.queue.put(lntx_id)
|
||||
r = await self.client.get(f"/lntx/{lntx_id}?fields=settled")
|
||||
data = r.json()
|
||||
if data["settled"]:
|
||||
await self.queue.put(lntx_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
+35
-43
@@ -30,12 +30,13 @@ class LnTipsWallet(Wallet):
|
||||
raise Exception("cannot initialize lntxbod")
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"Authorization": f"Basic {key}"}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/api/v1/balance", headers=self.auth, timeout=40
|
||||
)
|
||||
r = await self.client.get("/api/v1/balance", timeout=40)
|
||||
try:
|
||||
data = r.json()
|
||||
except:
|
||||
@@ -62,13 +63,11 @@ class LnTipsWallet(Wallet):
|
||||
elif unhashed_description:
|
||||
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/createinvoice",
|
||||
headers=self.auth,
|
||||
json=data,
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/api/v1/createinvoice",
|
||||
json=data,
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
try:
|
||||
@@ -85,13 +84,11 @@ class LnTipsWallet(Wallet):
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/payinvoice",
|
||||
headers=self.auth,
|
||||
json={"pay_req": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/api/v1/payinvoice",
|
||||
json={"pay_req": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
if r.is_error:
|
||||
return PaymentResponse(False, None, 0, None, r.text)
|
||||
|
||||
@@ -111,11 +108,9 @@ class LnTipsWallet(Wallet):
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/api/v1/invoicestatus/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.post(
|
||||
f"/api/v1/invoicestatus/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error or len(r.text) == 0:
|
||||
raise Exception
|
||||
@@ -127,11 +122,9 @@ class LnTipsWallet(Wallet):
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
url=f"{self.endpoint}/api/v1/paymentstatus/{checking_id}",
|
||||
headers=self.auth,
|
||||
)
|
||||
r = await self.client.post(
|
||||
url=f"/api/v1/paymentstatus/{checking_id}",
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
raise Exception
|
||||
@@ -145,23 +138,22 @@ class LnTipsWallet(Wallet):
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
last_connected = None
|
||||
while True:
|
||||
url = f"{self.endpoint}/api/v1/invoicestream"
|
||||
url = "/api/v1/invoicestream"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=None, headers=self.auth) as client:
|
||||
last_connected = time.time()
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
prefix = "data: "
|
||||
if not line.startswith(prefix):
|
||||
continue
|
||||
data = line[len(prefix) :] # sse parsing
|
||||
inv = json.loads(data)
|
||||
if not inv.get("payment_hash"):
|
||||
continue
|
||||
except:
|
||||
last_connected = time.time()
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for line in r.aiter_lines():
|
||||
try:
|
||||
prefix = "data: "
|
||||
if not line.startswith(prefix):
|
||||
continue
|
||||
yield inv["payment_hash"]
|
||||
data = line[len(prefix) :] # sse parsing
|
||||
inv = json.loads(data)
|
||||
if not inv.get("payment_hash"):
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
yield inv["payment_hash"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+21
-30
@@ -34,13 +34,14 @@ class OpenNodeWallet(Wallet):
|
||||
|
||||
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
self.auth = {"Authorization": key}
|
||||
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/account/balance", headers=self.auth, timeout=40
|
||||
)
|
||||
r = await self.client.get("/v1/account/balance", timeout=40)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
return StatusResponse(f"Unable to connect to '{self.endpoint}'", 0)
|
||||
|
||||
@@ -61,17 +62,15 @@ class OpenNodeWallet(Wallet):
|
||||
if description_hash or unhashed_description:
|
||||
raise Unsupported("description_hash")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/v1/charges",
|
||||
headers=self.auth,
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/v1/charges",
|
||||
json={
|
||||
"amount": amount,
|
||||
"description": memo or "",
|
||||
# "callback_url": url_for("/webhook_listener", _external=True),
|
||||
},
|
||||
timeout=40,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
@@ -83,13 +82,11 @@ class OpenNodeWallet(Wallet):
|
||||
return InvoiceResponse(True, checking_id, payment_request, None)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.post(
|
||||
f"{self.endpoint}/v2/withdrawals",
|
||||
headers=self.auth,
|
||||
json={"type": "ln", "address": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
r = await self.client.post(
|
||||
"/v2/withdrawals",
|
||||
json={"type": "ln", "address": bolt11},
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
if r.is_error:
|
||||
error_message = r.json()["message"]
|
||||
@@ -105,10 +102,7 @@ class OpenNodeWallet(Wallet):
|
||||
return PaymentResponse(True, checking_id, fee_msat, None, None)
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(f"/v1/charge/{checking_id}")
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
data = r.json()["data"]
|
||||
@@ -116,10 +110,7 @@ class OpenNodeWallet(Wallet):
|
||||
return PaymentStatus(statuses[data.get("status")])
|
||||
|
||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth
|
||||
)
|
||||
r = await self.client.get(f"/v1/withdrawal/{checking_id}")
|
||||
|
||||
if r.is_error:
|
||||
return PaymentStatus(None)
|
||||
|
||||
+20
-17
@@ -31,6 +31,13 @@ class SparkWallet(Wallet):
|
||||
assert settings.spark_url, "spark url does not exist"
|
||||
self.url = settings.spark_url.replace("/rpc", "")
|
||||
self.token = settings.spark_token
|
||||
assert self.token, "spark wallet token does not exist"
|
||||
self.client = httpx.AsyncClient(
|
||||
base_url=self.url, headers={"X-Access": self.token}
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
await self.client.aclose()
|
||||
|
||||
def __getattr__(self, key):
|
||||
async def call(*args, **kwargs):
|
||||
@@ -46,15 +53,12 @@ class SparkWallet(Wallet):
|
||||
params = {}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
assert self.token, "spark wallet token does not exist"
|
||||
r = await client.post(
|
||||
self.url + "/rpc",
|
||||
headers={"X-Access": self.token},
|
||||
json={"method": key, "params": params},
|
||||
timeout=60 * 60 * 24,
|
||||
)
|
||||
r.raise_for_status()
|
||||
r = await self.client.post(
|
||||
"/rpc",
|
||||
json={"method": key, "params": params},
|
||||
timeout=60 * 60 * 24,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except (
|
||||
OSError,
|
||||
httpx.ConnectError,
|
||||
@@ -224,17 +228,16 @@ class SparkWallet(Wallet):
|
||||
raise KeyError("supplied an invalid checking_id")
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
url = f"{self.url}/stream?access-key={self.token}"
|
||||
url = f"/stream?access-key={self.token}"
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=None) as client:
|
||||
async with client.stream("GET", url) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
data = json.loads(line[5:])
|
||||
if "pay_index" in data and data.get("status") == "paid":
|
||||
yield data["label"]
|
||||
async with self.client.stream("GET", url, timeout=None) as r:
|
||||
async for line in r.aiter_lines():
|
||||
if line.startswith("data:"):
|
||||
data = json.loads(line[5:])
|
||||
if "pay_index" in data and data.get("status") == "paid":
|
||||
yield data["label"]
|
||||
except (
|
||||
OSError,
|
||||
httpx.ReadError,
|
||||
|
||||
@@ -66,12 +66,15 @@
|
||||
"/static/i18n/fr.js",
|
||||
"/static/i18n/it.js",
|
||||
"/static/i18n/jp.js",
|
||||
"/static/i18n/cn.js",
|
||||
"/static/i18n/nl.js",
|
||||
"/static/i18n/pi.js",
|
||||
"/static/i18n/pl.js",
|
||||
"/static/i18n/fr.js",
|
||||
"/static/i18n/nl.js",
|
||||
"/static/i18n/we.js",
|
||||
"/static/i18n/pt.js",
|
||||
"/static/i18n/br.js",
|
||||
"/static/js/base.js",
|
||||
"/static/js/components.js",
|
||||
"/static/js/bolt11-decoder.js"
|
||||
|
||||
Generated
+168
-82
@@ -35,14 +35,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.15.3"
|
||||
version = "2.15.5"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
files = [
|
||||
{file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"},
|
||||
{file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"},
|
||||
{file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"},
|
||||
{file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -429,63 +429,63 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.2.3"
|
||||
version = "7.2.5"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
|
||||
{file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
|
||||
{file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-win32.whl", hash = "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5"},
|
||||
{file = "coverage-7.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-win32.whl", hash = "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b"},
|
||||
{file = "coverage-7.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-win32.whl", hash = "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718"},
|
||||
{file = "coverage-7.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-win32.whl", hash = "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813"},
|
||||
{file = "coverage-7.2.5-cp38-cp38-win_amd64.whl", hash = "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-win32.whl", hash = "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31"},
|
||||
{file = "coverage-7.2.5-cp39-cp39-win_amd64.whl", hash = "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252"},
|
||||
{file = "coverage-7.2.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3"},
|
||||
{file = "coverage-7.2.5.tar.gz", hash = "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -535,6 +535,24 @@ sdist = ["setuptools-rust (>=0.11.4)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
|
||||
|
||||
[[package]]
|
||||
name = "deprecated"
|
||||
version = "1.2.13"
|
||||
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
|
||||
{file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
wrapt = ">=1.10,<2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"]
|
||||
|
||||
[[package]]
|
||||
name = "dill"
|
||||
version = "0.3.6"
|
||||
@@ -802,14 +820,14 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.22"
|
||||
version = "2.5.24"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
|
||||
{file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
|
||||
{file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
|
||||
{file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -847,6 +865,25 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker
|
||||
perf = ["ipython"]
|
||||
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-resources"
|
||||
version = "5.12.0"
|
||||
description = "Read resources from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
|
||||
{file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
@@ -941,6 +978,37 @@ files = [
|
||||
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limits"
|
||||
version = "3.5.0"
|
||||
description = "Rate limiting utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "limits-3.5.0-py3-none-any.whl", hash = "sha256:3ad525faeb7e1c63859ca1cae34c9ed22a8f22c9ea9d96e2f412869f6b36beb9"},
|
||||
{file = "limits-3.5.0.tar.gz", hash = "sha256:b728c9ab3c6163997b1d11a51d252d951efd13f0d248ea2403383952498f8a22"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
deprecated = ">=1.2"
|
||||
importlib-resources = ">=1.3"
|
||||
packaging = ">=21,<24"
|
||||
setuptools = "*"
|
||||
typing-extensions = "*"
|
||||
|
||||
[package.extras]
|
||||
all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1)", "emcache (>=1)", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<5.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"]
|
||||
async-etcd = ["aetcd"]
|
||||
async-memcached = ["emcache (>=0.6.1)", "emcache (>=1)"]
|
||||
async-mongodb = ["motor (>=3,<4)"]
|
||||
async-redis = ["coredis (>=3.4.0,<5)"]
|
||||
etcd = ["etcd3"]
|
||||
memcached = ["pymemcache (>3,<5.0.0)"]
|
||||
mongodb = ["pymongo (>4.1,<5)"]
|
||||
redis = ["redis (>3,!=4.5.2,!=4.5.3,<5.0.0)"]
|
||||
rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "lnurl"
|
||||
version = "0.3.6"
|
||||
@@ -1143,14 +1211,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
description = "Node.js virtual environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||
files = [
|
||||
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
|
||||
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
|
||||
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
||||
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1197,19 +1265,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.2.0"
|
||||
version = "3.5.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
|
||||
{file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
|
||||
{file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"},
|
||||
{file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -1229,14 +1297,14 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.2.2"
|
||||
version = "3.3.2"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"},
|
||||
{file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"},
|
||||
{file = "pre_commit-3.3.2-py2.py3-none-any.whl", hash = "sha256:8056bc52181efadf4aac792b1f4f255dfd2fb5a350ded7335d251a68561e8cb6"},
|
||||
{file = "pre_commit-3.3.2.tar.gz", hash = "sha256:66e37bec2d882de1f17f88075047ef8962581f83c234ac08da21a0c58953d1f0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1443,18 +1511,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "2.17.2"
|
||||
version = "2.17.4"
|
||||
description = "python code static checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
files = [
|
||||
{file = "pylint-2.17.2-py3-none-any.whl", hash = "sha256:001cc91366a7df2970941d7e6bbefcbf98694e00102c1f121c531a814ddc2ea8"},
|
||||
{file = "pylint-2.17.2.tar.gz", hash = "sha256:1b647da5249e7c279118f657ca28b6aaebb299f86bf92affc632acf199f7adbb"},
|
||||
{file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"},
|
||||
{file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=2.15.2,<=2.17.0-dev0"
|
||||
astroid = ">=2.15.4,<=2.17.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = [
|
||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||
@@ -1819,6 +1887,24 @@ files = [
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slowapi"
|
||||
version = "0.1.8"
|
||||
description = "A rate limiting extension for Starlette and Fastapi"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "slowapi-0.1.8-py3-none-any.whl", hash = "sha256:629fc415575bbffcd9d8621cc3ce326a78402c5f9b7b50b127979118d485c72e"},
|
||||
{file = "slowapi-0.1.8.tar.gz", hash = "sha256:8cc268f5a7e3624efa3f7bd2859b895f9f2376c4ed4e0378dd2f7f3343ca608e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
limits = ">=2.3"
|
||||
|
||||
[package.extras]
|
||||
redis = ["redis (>=3.4.1,<4.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
@@ -1953,14 +2039,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.11.7"
|
||||
version = "0.11.8"
|
||||
description = "Style preserving TOML library"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomlkit-0.11.7-py3-none-any.whl", hash = "sha256:5325463a7da2ef0c6bbfefb62a3dc883aebe679984709aee32a317907d0a8d3c"},
|
||||
{file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
|
||||
{file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"},
|
||||
{file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2068,14 +2154,14 @@ test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOp
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.22.0"
|
||||
version = "20.23.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
|
||||
{file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
|
||||
{file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"},
|
||||
{file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2085,7 +2171,7 @@ platformdirs = ">=3.2,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
@@ -2173,7 +2259,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
name = "wrapt"
|
||||
version = "1.15.0"
|
||||
description = "Module for decorators, wrappers and monkey patching."
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
files = [
|
||||
@@ -2273,4 +2359,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "23981f78fb5f0da54fa4185d4fe7c009eda6b37a9372e7a471b6e10dc1058497"
|
||||
content-hash = "57f400dcbe045847d741d5291a0bc92a3b8cfd2af18970e370a8685087d45a39"
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "0.10.7"
|
||||
version = "0.10.9"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
@@ -33,6 +33,7 @@ Cerberus = "1.3.4"
|
||||
async-timeout = "4.0.2"
|
||||
pyln-client = "0.11.1"
|
||||
cashu = "0.9.0"
|
||||
slowapi = "^0.1.7"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "^6.0.0"
|
||||
@@ -65,6 +66,7 @@ include = [
|
||||
exclude = [
|
||||
"lnbits/wallets/lnd_grpc_files",
|
||||
"lnbits/extensions",
|
||||
"lnbits/upgrades",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
@@ -127,6 +129,7 @@ enable = [
|
||||
[tool.pylint.MASTER]
|
||||
ignore-paths = [
|
||||
"^lnbits/extensions/.*$",
|
||||
"^lnbits/upgrades/.*$",
|
||||
"^lnbits/wallets/lnd_grpc_files/.*$",
|
||||
]
|
||||
fail-under = 10.0
|
||||
|
||||
+2
-2
@@ -127,6 +127,6 @@ async def invoice(to_wallet):
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def real_invoice():
|
||||
invoice = get_real_invoice(100_000, "test-fixture")
|
||||
yield invoice
|
||||
invoice = get_real_invoice(100)
|
||||
yield {"bolt11": invoice["payment_request"]}
|
||||
del invoice
|
||||
|
||||
@@ -6,12 +6,13 @@ import pytest
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.views.admin_api import api_auditor
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.db import DB_TYPE, SQLITE
|
||||
from lnbits.settings import get_wallet_class
|
||||
from lnbits.wallets import get_wallet_class
|
||||
from tests.conftest import CreateInvoiceData, api_payments_create_invoice
|
||||
|
||||
from ...helpers import get_random_invoice_data, is_fake
|
||||
from ...helpers import get_random_invoice_data, is_fake, pay_real_invoice
|
||||
|
||||
WALLET = get_wallet_class()
|
||||
|
||||
@@ -320,11 +321,17 @@ async def test_create_invoice_with_unhashed_description(client, inkey_headers_to
|
||||
return invoice
|
||||
|
||||
|
||||
async def get_node_balance_sats():
|
||||
audit = await api_auditor()
|
||||
return audit["node_balance_msats"] / 1000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_pay_real_invoice(
|
||||
client, real_invoice, adminkey_headers_from, inkey_headers_from
|
||||
):
|
||||
prev_balance = await get_node_balance_sats()
|
||||
response = await client.post(
|
||||
"/api/v1/payments", json=real_invoice, headers=adminkey_headers_from
|
||||
)
|
||||
@@ -337,5 +344,46 @@ async def test_pay_real_invoice(
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert type(response) == dict
|
||||
assert response["paid"] is True
|
||||
assert response["paid"]
|
||||
|
||||
status = await WALLET.get_payment_status(invoice["payment_hash"])
|
||||
assert status.paid
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
balance = await get_node_balance_sats()
|
||||
assert prev_balance - balance == 100
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(is_fake, reason="this only works in regtest")
|
||||
async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_from):
|
||||
prev_balance = await get_node_balance_sats()
|
||||
create_invoice = CreateInvoiceData(out=False, amount=1000, memo="test")
|
||||
response = await client.post(
|
||||
"/api/v1/payments",
|
||||
json=create_invoice.dict(),
|
||||
headers=adminkey_headers_from,
|
||||
)
|
||||
assert response.status_code < 300
|
||||
invoice = response.json()
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert not response["paid"]
|
||||
|
||||
async def listen():
|
||||
async for payment_hash in get_wallet_class().paid_invoices_stream():
|
||||
assert payment_hash == invoice["payment_hash"]
|
||||
return
|
||||
|
||||
task = asyncio.create_task(listen())
|
||||
pay_real_invoice(invoice["payment_request"])
|
||||
await asyncio.wait_for(task, timeout=3)
|
||||
response = await api_payment(
|
||||
invoice["payment_hash"], inkey_headers_from["X-Api-Key"]
|
||||
)
|
||||
assert response["paid"]
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
balance = await get_node_balance_sats()
|
||||
assert balance - prev_balance == create_invoice.amount
|
||||
|
||||
+10
-10
@@ -3,11 +3,10 @@ import json
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
import time
|
||||
from subprocess import PIPE, Popen, run
|
||||
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.settings import get_wallet_class
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
|
||||
|
||||
async def credit_wallet(wallet_id: str, amount: int):
|
||||
@@ -38,6 +37,7 @@ async def get_random_invoice_data():
|
||||
return {"out": False, "amount": 10, "memo": f"test_memo_{get_random_string(10)}"}
|
||||
|
||||
|
||||
set_wallet_class()
|
||||
WALLET = get_wallet_class()
|
||||
is_fake: bool = WALLET.__class__.__name__ == "FakeWallet"
|
||||
is_regtest: bool = not is_fake
|
||||
@@ -47,8 +47,8 @@ docker_bitcoin_rpc = "lnbits"
|
||||
docker_prefix = "lnbits-legend"
|
||||
docker_cmd = "docker exec"
|
||||
|
||||
docker_lightning = f"{docker_cmd} {docker_prefix}-clightning-2-1"
|
||||
docker_lightning_cli = f"{docker_lightning} lightning-cli --network regtest"
|
||||
docker_lightning = f"{docker_cmd} {docker_prefix}-lnd-1-1"
|
||||
docker_lightning_cli = f"{docker_lightning} lncli --network regtest --rpcserver=lnd-1"
|
||||
|
||||
docker_bitcoin = f"{docker_cmd} {docker_prefix}-bitcoind-1-1"
|
||||
docker_bitcoin_cli = f"{docker_bitcoin} bitcoin-cli -rpcuser={docker_bitcoin_rpc} -rpcpassword={docker_bitcoin_rpc} -regtest"
|
||||
@@ -62,16 +62,16 @@ def run_cmd_json(cmd: str) -> dict:
|
||||
return json.loads(run_cmd(cmd))
|
||||
|
||||
|
||||
def get_real_invoice(sats: int, prefix: str, description: str = "test") -> dict:
|
||||
msats = sats * 1000
|
||||
return run_cmd_json(
|
||||
f"{docker_lightning_cli} invoice {msats} {prefix}-{time.time()} {description}"
|
||||
)
|
||||
def get_real_invoice(sats: int) -> dict:
|
||||
return run_cmd_json(f"{docker_lightning_cli} addinvoice {sats}")
|
||||
|
||||
|
||||
def pay_real_invoice(invoice: str) -> Popen:
|
||||
return Popen(
|
||||
f"{docker_lightning_cli} pay {invoice}", shell=True, stdin=PIPE, stdout=PIPE
|
||||
f"{docker_lightning_cli} payinvoice --force {invoice}",
|
||||
shell=True,
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user