Compare commits

...
25 Commits
Author SHA1 Message Date
dni ⚡andGitHub dbb689c5c5 chore: update version to 0.12.10 (#2597) 2024-07-23 14:06:55 +02:00
dni ⚡andGitHub 2167aa398f fix: annotations for models.py (#2595) 2024-07-23 14:03:27 +02:00
dni ⚡andGitHub eb8d2f312f fix: install extensions async (#2596)
so it does not block webserver start on saas instances and comes up
faster if extensions are reinstalled
2024-07-23 14:01:34 +02:00
jackstar12andGitHub f9133760fc fix: proper status check in invoice paid callback (#2592)
status fields like preimage and fee_msat are never updated otherwise
2024-07-22 16:59:26 +02:00
7298c4664b feat: Blink funding source (#2477)
* feat: Blink funding source

* chore: make bundle

* Blink review 01 (#2575)

* refactor: mark `graphql_query` as private (`_` prefix)

* feat: set default value for `blink_api_endpoint`

* fix: raise if HTTP call failed

* refactor: move private method to the bottom

* refactor: make `wallet_id` a property

* fix: key mapping for attribute

* chore: fix `mypy`

* chore: fix `make check`

* refactor: extract query strings

* refactor: extract `BlinkGrafqlQueries` class

* chore: code clean-up

* chore: add `try-catch`

* refactor: extract `tx_query`

* chore: format grapfhql queries

* fix: set funding source class

* chore: `make format`

* fix: test by following the other patterns

* Update docs/guide/wallets.md

Co-authored-by: openoms <43343391+openoms@users.noreply.github.com>

* feat: add websocket connection to blink (#2577)

* feat: add websocket connection to blink

* feat: close websocket on shutdown

* feat: add `blink_ws_endpoint` to the UI

* fix: use `SEND` tx for `settlementFee`

* refactor: remove `else` when `if` has `return`

* fix: remove test env file

---------

Co-authored-by: bitkarrot <73979971+bitkarrot@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: openoms <43343391+openoms@users.noreply.github.com>
2024-07-19 21:32:02 +01:00
Pavol RusnakandGitHub eda7e35c61 chore: update python deps (#2586) 2024-07-16 07:59:04 +02:00
7d1e22c7de fix: always create default wallet for user (#2580)
* fix: always create default wallet for user

* no assert in api

---------

Co-authored-by: dni  <office@dnilabs.com>
2024-07-15 13:34:26 +03:00
michael1011andGitHub 1bee84d419 chore: optimize Dockerfile (#2576)
By doing a multi-stage Docker build, the size
of the final image is reduced by ~45%.
2024-07-10 14:22:44 +02:00
a00292544f fix: lib secp256k fails building (#2572)
Co-authored-by: michael1011 <me@michael1011.at>
2024-07-09 15:51:40 +02:00
ArcandGitHub fe14c2cd83 Fixed ugly adv description card (#2570) 2024-07-09 14:03:11 +01:00
schneimiandGitHub 760f11f1ce Update wallet.js (#2569)
enable 'Read' button after 'Paste from clipboard' click
2024-07-09 13:57:03 +01:00
0e1090b717 docs: nginx websocket support for reverse proxy (#2564)
* docs: nginx websocket support for reverse proxy

websocket weren't working with that conf

* cleanup

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-07-09 13:56:31 +01:00
Vlad StanandGitHub b2564154cd [feat] add default_user_extensions setting (#2571)
* feat: add `lnbits_user_default_extensions` to `settings`

* refactor: extract `create_user_account` in services

* feat: auto enable user extensions
2024-07-09 13:55:05 +01:00
Vlad StanandGitHub fb17611207 [feat] add authenticated_user_id decorator (#2566)
* feat: add `optional_user_id` decorator
2024-07-04 17:24:20 +03:00
ArcandGitHub 7f628948c9 bug: carousel and adv desc fix (#2562)
* Carousel image fix
* pushed carousel nav to top as well so yt controls are accessible
* Added some extra breathing room for the adv description
2024-06-26 13:21:34 +02:00
dni ⚡andGitHub cbe0861439 feat: improve on extension page layout (#2558)
* feat: improve on extension page layout
give it more luft and simplify it a bit. also improved responsiveness
* show description now, ben
2024-06-26 13:07:13 +02:00
dni ⚡andGitHub e9d6160f4d chore: prepare for 0.12.9 (#2555) 2024-06-19 12:56:34 +02:00
Vlad StanandGitHub eacdd432b2 [feat] Extension details page (#2544)
* feat: add empty dialog

* feat: add `details_link` field for extension

* feat: show info icon if `details_link` present

* feat: add extension details endpoint

* feat: first details page

* feat: carousel working

* feat: full screen

* fix: layout

* fix: repo site

* fix: release icon

* fix: repo link

* feat: terms and conditions partial

* chore: fix typing

* fix: info icon layout

* chore: add try-catch

* feat: layout improvements

* feat: add video link

* fix: show terms and conditions

* chore: code format

* feat: add `details_link`

* fix: github release details

* feat: add close button

* chore: code clean-up

* chore: revert some changes

* feat: i18n

* chore: `make bundle`

* chore: make bundle

* feat: terms and conditions is a link now
2024-06-19 11:52:18 +01:00
Pavol RusnakandGitHub 76e8d72d0d LNBits -> LNbits typo (#2552)
* LNBits -> LNbits
2024-06-19 09:27:26 +03:00
dbacf7e8c1 "Failed to connect to https://ws:" crash (#2548)
* Update base.py

fixing bug in normalize_enpoints

* Update lnbits/wallets/base.py

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-06-17 17:36:15 +02:00
dni ⚡andGitHub b6d99b09cf fix: npm packages with high severity issue (#2551)
updating `braces` npm package
2024-06-17 17:17:53 +02:00
5c21e7f9ed Update installation.md (#2549)
Minor rewording

---------

Co-authored-by: dni  <office@dnilabs.com>
2024-06-17 11:24:31 +02:00
14e9c7d9dc Fix typo of "LNbits" in list of funding sources. (#2546)
* Fix typo of "LNbits" in list of funding sources.


---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-06-17 09:08:22 +02:00
blackcoffeexbtandGitHub b3368d89f4 Nice formatting of funding source titles in Server admin (#2543)
* Display friendly funding source titles in funding sources list

* Sort funding options select alphabetically

* Run make bundle
2024-06-10 23:37:09 +02:00
83b89851a5 fix: phoenixd wallet description field supports lnurlp (#2514)
* Fix for phoenixd and lnurlp nostr usage

- Support description, restricted to 128 characters
- Support descriptionHash

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: Vic <vic@example.com>
2024-05-29 13:47:10 +03:00
54 changed files with 1890 additions and 865 deletions
+1
View File
@@ -3,6 +3,7 @@ data
docker
docs
tests
node_modules
lnbits/static/css/*
lnbits/static/bundle.js
+8 -1
View File
@@ -28,7 +28,7 @@ PORT=5000
######################################
# which fundingsources are allowed in the admin ui
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet"
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
@@ -91,6 +91,11 @@ ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
ZBD_API_KEY=ZBD_ACCESS_TOKEN
# BlinkWallet
BLINK_API_ENDPOINT=https://api.blink.sv/graphql
BLINK_WS_ENDPOINT=wss://ws.blink.sv/graphql
BLINK_TOKEN=BLINK_TOKEN
# PhoenixdWallet
PHOENIXD_API_ENDPOINT=http://localhost:9740/
PHOENIXD_API_PASSWORD=PHOENIXD_KEY
@@ -162,6 +167,8 @@ LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Extensions enabled by default when a user is created
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
# Start LNbits core only. The extensions are not loaded.
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
+4
View File
@@ -43,6 +43,10 @@ jobs:
pypi:
runs-on: ubuntu-latest
steps:
- name: Install dependencies for building secp256k1
run: |
sudo apt-get update
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
- uses: actions/checkout@v4
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v1.15
+34 -9
View File
@@ -1,4 +1,4 @@
FROM python:3.10-slim-bookworm
FROM python:3.10-slim-bookworm AS builder
RUN apt-get clean
RUN apt-get update
@@ -7,19 +7,44 @@ RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
WORKDIR /app
# Only copy the files required to install the dependencies
COPY pyproject.toml poetry.lock ./
RUN mkdir data
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN poetry install --only main
FROM python:3.10-slim-bookworm
# needed for backups postgresql-client version 14 (pg_dump)
RUN apt install -y postgresql-common ca-certificates
RUN install -d /usr/share/postgresql-common/pgdg
RUN curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
RUN sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
RUN apt-get update
RUN apt-get install -y postgresql-client-14
RUN apt-get update && apt-get -y upgrade && \
apt-get -y install gnupg2 curl lsb-release && \
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \
curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
apt-get update && \
apt-get -y install postgresql-client-14 postgresql-client-common && \
apt-get clean all && rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
WORKDIR /app
COPY . .
RUN mkdir data
COPY --from=builder /app/.venv .venv
RUN poetry install --only main
+11 -2
View File
@@ -6,9 +6,9 @@ nav_order: 2
# Basic installation
You can choose between four package managers, `poetry` and `nix`
The following sections explain how to install LNbits using varions package managers: `poetry`, `nix`, `Docker` and `Fly.io`.
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
## Option 1 (recommended): poetry
@@ -444,6 +444,15 @@ server {
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
+9
View File
@@ -76,6 +76,15 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
### Blink
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate a Blink API key after logging in or creating a new Blink account at: https://dashboard.blink.sv. For more info visit: https://dev.blink.sv/api/auth#create-an-api-key```
- `LNBITS_BACKEND_WALLET_CLASS`: **BlinkWallet**
- `BLINK_API_ENDPOINT`: https://api.blink.sv/graphql
- `BLINK_WS_ENDPOINT`: wss://ws.blink.sv/graphql
- `BLINK_TOKEN`: BlinkToken
### Alby
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
+6
View File
@@ -37,6 +37,12 @@
asgi-lifespan = prev.asgi-lifespan.overridePythonAttrs (
old: { buildInputs = (old.buildInputs or []) ++ [ prev.setuptools ]; }
);
dnspython = prev.dnspython.overridePythonAttrs (
old: { buildInputs = (old.buildInputs or []) ++ [ prev.hatchling ]; }
);
jinja2 = prev.jinja2.overridePythonAttrs (
old: { buildInputs = (old.buildInputs or []) ++ [ prev.flit-core ]; }
);
pytest-md = prev.pytest-md.overridePythonAttrs (
old: { buildInputs = (old.buildInputs or []) ++ [ prev.setuptools ]; }
);
+13 -11
View File
@@ -63,6 +63,7 @@ from .middleware import (
from .requestvars import g
from .tasks import (
check_pending_payments,
create_task,
internal_invoice_listener,
invoice_listener,
)
@@ -93,13 +94,8 @@ async def startup(app: FastAPI):
# register core routes
init_core_routers(app)
# check extensions after restart
if not settings.lnbits_extensions_deactivate_all:
await check_installed_extensions(app)
register_all_ext_routes(app)
# initialize tasks
register_async_tasks()
register_async_tasks(app)
async def shutdown():
@@ -399,22 +395,28 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
app.include_router(router=ext_route, prefix=prefix)
def register_all_ext_routes(app: FastAPI):
async def check_and_register_extensions(app: FastAPI):
await check_installed_extensions(app)
for ext in get_valid_extensions(False):
try:
register_ext_routes(app, ext)
except Exception as e:
logger.error(f"Could not load extension `{ext.code}`: {e!s}")
except Exception as exc:
logger.error(f"Could not load extension `{ext.code}`: {exc!s}")
def register_async_tasks():
def register_async_tasks(app: FastAPI):
# check extensions after restart
if not settings.lnbits_extensions_deactivate_all:
create_task(check_and_register_extensions(app))
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
# core invoice listener
invoice_queue = asyncio.Queue(5)
invoice_queue: asyncio.Queue = asyncio.Queue(5)
register_invoice_listener(invoice_queue, "core")
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
+8 -49
View File
@@ -2,7 +2,7 @@ import datetime
import json
from time import time
from typing import Any, Dict, List, Literal, Optional
from uuid import UUID, uuid4
from uuid import uuid4
import shortuuid
from passlib.context import CryptContext
@@ -26,7 +26,6 @@ from lnbits.settings import (
from .models import (
Account,
AccountFilters,
CreateUser,
Payment,
PaymentFilters,
PaymentHistoryPoint,
@@ -42,63 +41,23 @@ from .models import (
# --------
async def create_user(
data: CreateUser, user_config: Optional[UserConfig] = None
) -> User:
if not settings.new_accounts_allowed:
raise ValueError("Account creation is disabled.")
if await get_account_by_username(data.username):
raise ValueError("Username already exists.")
if data.email and await get_account_by_email(data.email):
raise ValueError("Email already exists.")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user_id = uuid4().hex
tsph = db.timestamp_placeholder
now = int(time())
await db.execute(
f"""
INSERT INTO accounts
(id, email, username, pass, extra, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
""",
(
user_id,
data.email,
data.username,
pwd_context.hash(data.password),
json.dumps(dict(user_config)) if user_config else "{}",
now,
now,
),
)
new_account = await get_account(user_id=user_id)
assert new_account, "Newly created account couldn't be retrieved"
return new_account
async def create_account(
conn: Optional[Connection] = None,
user_id: Optional[str] = None,
username: Optional[str] = None,
email: Optional[str] = None,
password: Optional[str] = None,
user_config: Optional[UserConfig] = None,
conn: Optional[Connection] = None,
) -> User:
if user_id:
user_uuid4 = UUID(hex=user_id, version=4)
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
else:
user_id = uuid4().hex
user_id = user_id or uuid4().hex
extra = json.dumps(dict(user_config)) if user_config else "{}"
now = int(time())
await (conn or db).execute(
f"""
INSERT INTO accounts (id, email, extra, created_at, updated_at)
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
INSERT INTO accounts (id, username, pass, email, extra, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
""",
(user_id, email, extra, now, now),
(user_id, username, password, email, extra, now, now),
)
new_account = await get_account(user_id=user_id, conn=conn)
+10 -8
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import datetime
import hashlib
import hmac
@@ -6,7 +8,7 @@ import time
from dataclasses import dataclass
from enum import Enum
from sqlite3 import Row
from typing import Callable, Dict, List, Optional
from typing import Callable, Optional
from ecdsa import SECP256k1, SigningKey
from fastapi import Query
@@ -62,7 +64,7 @@ class Wallet(BaseWallet):
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
)
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
async def get_payment(self, payment_hash: str) -> Optional[Payment]:
from .crud import get_standalone_payment
return await get_standalone_payment(payment_hash)
@@ -132,8 +134,8 @@ class User(BaseModel):
id: str
email: Optional[str] = None
username: Optional[str] = None
extensions: List[str] = []
wallets: List[Wallet] = []
extensions: list[str] = []
wallets: list[Wallet] = []
admin: bool = False
super_user: bool = False
has_password: bool = False
@@ -142,10 +144,10 @@ class User(BaseModel):
updated_at: Optional[int] = None
@property
def wallet_ids(self) -> List[str]:
def wallet_ids(self) -> list[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@@ -208,7 +210,7 @@ class Payment(FromRowModel):
preimage: str
payment_hash: str
expiry: Optional[float]
extra: Dict = {}
extra: dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
@@ -349,7 +351,7 @@ class PaymentFilters(FilterModel):
preimage: str
payment_hash: str
expiry: Optional[datetime.datetime]
extra: Dict = {}
extra: dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
+41 -1
View File
@@ -6,12 +6,14 @@ from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypedDict
from urllib.parse import parse_qs, urlparse
from uuid import UUID, uuid4
import httpx
from bolt11 import decode as bolt11_decode
from cryptography.hazmat.primitives import serialization
from fastapi import Depends, WebSocket
from loguru import logger
from passlib.context import CryptContext
from py_vapid import Vapid
from py_vapid.utils import b64urlencode
@@ -50,6 +52,8 @@ from .crud import (
create_wallet,
delete_wallet_payment,
get_account,
get_account_by_email,
get_account_by_username,
get_payments,
get_standalone_payment,
get_super_settings,
@@ -60,9 +64,10 @@ from .crud import (
update_payment_details,
update_payment_status,
update_super_user,
update_user_extension,
)
from .helpers import to_valid_user_id
from .models import BalanceDelta, Payment, UserConfig, Wallet
from .models import BalanceDelta, Payment, User, UserConfig, Wallet
class PaymentError(Exception):
@@ -775,6 +780,41 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
return await create_admin_settings(account.id, editable_settings.dict())
async def create_user_account(
user_id: Optional[str] = None,
email: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
wallet_name: Optional[str] = None,
user_config: Optional[UserConfig] = None,
) -> User:
if not settings.new_accounts_allowed:
raise ValueError("Account creation is disabled.")
if username and await get_account_by_username(username):
raise ValueError("Username already exists.")
if email and await get_account_by_email(email):
raise ValueError("Email already exists.")
if user_id:
user_uuid4 = UUID(hex=user_id, version=4)
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
else:
user_id = uuid4().hex
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
password = pwd_context.hash(password) if password else None
account = await create_account(user_id, username, email, password, user_config)
wallet = await create_wallet(user_id=account.id, wallet_name=wallet_name)
account.wallets = [wallet]
for ext_id in settings.lnbits_user_default_extensions:
await update_user_extension(user_id=account.id, extension=ext_id, active=True)
return account
class WebsocketConnectionManager:
def __init__(self) -> None:
self.active_connections: List[WebSocket] = []
+87 -74
View File
@@ -45,56 +45,7 @@
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.code)"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Miscellaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Disable Extensions</q-item-label>
<q-item-label caption>Disables all extensions</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_extensions_deactivate_all"
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>Hide API</q-item-label>
<q-item-label caption
>Hides wallet api, extensions can choose to honor</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_hide_api"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
<br />
<h6 class="q-my-none">Service Fee</h6>
<div class="row q-col-gutter-md">
@@ -154,32 +105,94 @@
<br />
</div>
</div>
<q-separator></q-separator>
<h6 class="q-my-none">Extensions</h6>
<div>
<p>Extension Sources</p>
<q-input
filled
v-model="formAddExtensionsManifest"
@keydown.enter="addExtensionsManifest"
type="text"
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
hint="Repositories from where the extensions can be downloaded"
>
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="manifestUrl in formData.lnbits_extensions_manifests"
:key="manifestUrl"
removable
@remove="removeExtensionsManifest(manifestUrl)"
color="primary"
text-color="white"
><span v-text="manifestUrl"></span
></q-chip>
<div class="row q-col-gutter-md">
<div class="col-12">
<p>Extension Sources</p>
<q-input
filled
v-model="formAddExtensionsManifest"
@keydown.enter="addExtensionsManifest"
type="text"
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
hint="Repositories from where the extensions can be downloaded"
>
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="manifestUrl in formData.lnbits_extensions_manifests"
:key="manifestUrl"
removable
@remove="removeExtensionsManifest(manifestUrl)"
color="primary"
text-color="white"
><span v-text="manifestUrl"></span
></q-chip>
</div>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.code)"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>User Default Extensions</p>
<q-select
filled
v-model="formData.lnbits_user_default_extensions"
multiple
hint="Extensions that will be enabled by default for the users."
label="User extensions"
:options="g.extensions.map(e => e.code)"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>Miscellaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Disable Extensions</q-item-label>
<q-item-label caption>Disables all extensions</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_extensions_deactivate_all"
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>Hide API</q-item-label>
<q-item-label caption
>Hides wallet api, extensions can choose to honor</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_hide_api"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
<br />
</div>
</div>
</q-card-section>
File diff suppressed because it is too large Load Diff
+3 -7
View File
@@ -36,11 +36,7 @@ from lnbits.utils.exchange_rates import (
satoshis_amount_as_fiat,
)
from ..crud import (
create_account,
create_wallet,
)
from ..services import perform_lnurlauth
from ..services import create_user_account, perform_lnurlauth
# backwards compatibility for extension
# TODO: remove api_payment and pay_invoice imports from extensions
@@ -70,8 +66,8 @@ async def api_create_account(data: CreateWallet) -> Wallet:
status_code=HTTPStatus.FORBIDDEN,
detail="Account creation is disabled.",
)
account = await create_account()
return await create_wallet(user_id=account.id, wallet_name=data.name)
account = await create_user_account(wallet_name=data.name)
return account.wallets[0]
@api_router.get("/api/v1/lnurlscan/{code}")
+5 -4
View File
@@ -12,6 +12,7 @@ from starlette.status import (
HTTP_500_INTERNAL_SERVER_ERROR,
)
from lnbits.core.services import create_user_account
from lnbits.decorators import check_user_exists
from lnbits.helpers import (
create_access_token,
@@ -23,8 +24,6 @@ from lnbits.helpers import (
from lnbits.settings import AuthMethods, settings
from ..crud import (
create_account,
create_user,
get_account,
get_account_by_email,
get_account_by_username_or_email,
@@ -166,7 +165,9 @@ async def register(data: CreateUser) -> JSONResponse:
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
try:
user = await create_user(data)
user = await create_user_account(
email=data.email, username=data.username, password=data.password
)
return _auth_success_response(user.username)
except ValueError as exc:
@@ -274,7 +275,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
else:
if not settings.new_accounts_allowed:
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
user = await create_account(email=email, user_config=user_config)
user = await create_user_account(email=email, user_config=user_config)
if not user:
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
+30
View File
@@ -37,6 +37,7 @@ from lnbits.extension_manager import (
ReleasePaymentInfo,
UserExtensionInfo,
fetch_github_release_config,
fetch_release_details,
fetch_release_payment_info,
get_valid_extensions,
)
@@ -128,6 +129,35 @@ async def api_install_extension(
) from exc
@extension_router.get("/{ext_id}/details", dependencies=[Depends(check_user_exists)])
async def api_extension_details(
ext_id: str,
details_link: str,
):
try:
all_releases = await InstallableExtension.get_extension_releases(ext_id)
release = next(
(r for r in all_releases if r.details_link == details_link), None
)
assert release, "Details not found for release"
release_details = await fetch_release_details(details_link)
assert release_details, "Cannot fetch details for release"
release_details["icon"] = release.icon
release_details["repo"] = release.repo
return release_details
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR,
f"Failed to get details for extension {ext_id}.",
) from exc
@extension_router.put("/{ext_id}/sell")
async def api_update_pay_to_enable(
ext_id: str,
+13
View File
@@ -158,6 +158,19 @@ async def check_user_exists(
return user
async def optional_user_id(
access_token: Annotated[Optional[str], Depends(check_access_token)],
usr: Optional[UUID4] = None,
) -> Optional[str]:
if usr and settings.is_auth_method_allowed(AuthMethods.user_id_only):
return usr.hex
if access_token:
account = await _get_account_from_token(access_token)
return account.id if account else None
return None
async def check_admin(user: Annotated[User, Depends(check_user_exists)]) -> User:
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
raise HTTPException(
+34 -3
View File
@@ -32,6 +32,7 @@ class ExplicitRelease(BaseModel):
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
details_link: Optional[str]
pay_link: Optional[str]
def is_version_compatible(self):
@@ -58,6 +59,9 @@ class GitHubRepoRelease(BaseModel):
zipball_url: str
html_url: str
def details_link(self, source_repo: str) -> str:
return f"https://raw.githubusercontent.com/{source_repo}/{self.tag_name}/config.json"
class GitHubRepo(BaseModel):
stargazers_count: str
@@ -210,6 +214,24 @@ async def fetch_release_payment_info(
return None
async def fetch_release_details(details_link: str) -> Optional[dict]:
try:
async with httpx.AsyncClient() as client:
resp = await client.get(details_link)
resp.raise_for_status()
data = resp.json()
if "description_md" in data:
resp = await client.get(data["description_md"])
if not resp.is_error:
data["description_md"] = resp.text
return data
except Exception as e:
logger.warning(e)
return None
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
@@ -315,6 +337,7 @@ class ExtensionRelease(BaseModel):
warning: Optional[str] = None
repo: Optional[str] = None
icon: Optional[str] = None
details_link: Optional[str] = None
pay_link: Optional[str] = None
cost_sats: Optional[int] = None
@@ -347,6 +370,7 @@ class ExtensionRelease(BaseModel):
archive=r.zipball_url,
source_repo=source_repo,
is_github_release=True,
details_link=r.details_link(source_repo),
repo=f"https://github.com/{source_repo}",
html_url=r.html_url,
)
@@ -366,6 +390,7 @@ class ExtensionRelease(BaseModel):
is_version_compatible=e.is_version_compatible(),
warning=e.warning,
html_url=e.html_url,
details_link=e.details_link,
pay_link=e.pay_link,
repo=e.repo,
icon=e.icon,
@@ -613,18 +638,18 @@ class InstallableExtension(BaseModel):
repo, latest_release, config = await fetch_github_repo_info(
github_release.organisation, github_release.repository
)
source_repo = f"{github_release.organisation}/{github_release.repository}"
return InstallableExtension(
id=github_release.id,
name=config.name,
short_description=config.short_description,
stars=int(repo.stargazers_count),
icon=icon_to_github_url(
f"{github_release.organisation}/{github_release.repository}",
source_repo,
config.tile,
),
latest_release=ExtensionRelease.from_github_release(
repo.html_url, latest_release
source_repo, latest_release
),
)
except Exception as e:
@@ -740,6 +765,12 @@ class CreateExtension(BaseModel):
payment_hash: Optional[str] = None
class ExtensionDetailsRequest(BaseModel):
ext_id: str
source_repo: str
version: str
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
valid_extensions = [
extension for extension in ExtensionManager().extensions if extension.is_valid
+1 -1
View File
@@ -16,7 +16,7 @@ from lnbits.settings import set_cli_settings, settings
}
)
@click.option("--port", default=settings.port, help="Port to listen on")
@click.option("--host", default=settings.host, help="Host to run LNBits on")
@click.option("--host", default=settings.host, help="Host to run LNbits on")
@click.option(
"--forwarded-allow-ips",
default=settings.forwarded_allow_ips,
+12 -3
View File
@@ -47,6 +47,7 @@ class UsersSettings(LNbitsSettings):
class ExtensionsSettings(LNbitsSettings):
lnbits_admin_extensions: list[str] = Field(default=[])
lnbits_user_default_extensions: list[str] = Field(default=[])
lnbits_extensions_deactivate_all: bool = Field(default=False)
lnbits_extensions_manifests: list[str] = Field(
default=[
@@ -217,6 +218,12 @@ class LnPayFundingSource(LNbitsSettings):
lnpay_admin_key: Optional[str] = Field(default=None)
class BlinkFundingSource(LNbitsSettings):
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
blink_token: Optional[str] = Field(default=None)
class ZBDFundingSource(LNbitsSettings):
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
zbd_api_key: Optional[str] = Field(default=None)
@@ -265,6 +272,7 @@ class FundingSourcesSettings(
LndRestFundingSource,
LndGrpcFundingSource,
LnPayFundingSource,
BlinkFundingSource,
AlbyFundingSource,
ZBDFundingSource,
PhoenixdFundingSource,
@@ -414,14 +422,15 @@ class SuperUserSettings(LNbitsSettings):
lnbits_allowed_funding_sources: list[str] = Field(
default=[
"AlbyWallet",
"FakeWallet",
"BlinkWallet",
"CoreLightningRestWallet",
"CoreLightningWallet",
"EclairWallet",
"LNbitsWallet",
"LndRestWallet",
"FakeWallet",
"LNPayWallet",
"LNbitsWallet",
"LnTipsWallet",
"LndRestWallet",
"LndWallet",
"OpenNodeWallet",
"PhoenixdWallet",
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -9
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -554,3 +554,12 @@ video {
.whitespace-pre-line {
white-space: pre-line;
}
.q-carousel__slide {
background-size: contain;
background-repeat: no-repeat;
}
.q-dialog__inner--minimized {
padding: 12px;
}
+1 -1
View File
@@ -169,7 +169,7 @@ window.localisation.br = {
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
killswitch_interval: 'Intervalo do Killswitch',
killswitch_interval_desc:
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc:
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
+1 -1
View File
@@ -159,7 +159,7 @@ window.localisation.cn = {
'如果启用,当LNbits发送终止信号时,系统将自动将您的资金来源更改为VoidWallet。更新后,您将需要手动启用。',
killswitch_interval: 'Killswitch 间隔',
killswitch_interval_desc:
'后台任务应该多久检查一次来自状态源的LNBits断路信号(以分钟为单位)。',
'后台任务应该多久检查一次来自状态源的LNbits断路信号(以分钟为单位)。',
enable_watchdog: '启用看门狗',
enable_watchdog_desc:
'如果启用,当您的余额低于LNbits余额时,系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
+1 -1
View File
@@ -166,7 +166,7 @@ window.localisation.cs = {
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
killswitch_interval: 'Interval Killswitch',
killswitch_interval_desc:
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNBits ze zdroje stavu (v minutách).',
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNbits ze zdroje stavu (v minutách).',
enable_watchdog: 'Povolit Watchdog',
enable_watchdog_desc:
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
+1 -1
View File
@@ -171,7 +171,7 @@ window.localisation.de = {
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
killswitch_interval: 'Intervall für den Notausschalter',
killswitch_interval_desc:
'Wie oft die Hintergrundaufgabe nach dem LNBits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
'Wie oft die Hintergrundaufgabe nach dem LNbits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
enable_watchdog: 'Aktiviere Watchdog',
enable_watchdog_desc:
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
+4 -2
View File
@@ -165,7 +165,7 @@ window.localisation.en = {
'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).',
'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.',
@@ -258,5 +258,7 @@ window.localisation.en = {
sell_info:
'The %{name} extension requires a payment of minimum %{amount} sats to enable.',
hide_empty_wallets: 'Hide empty wallets',
recheck: 'Recheck'
recheck: 'Recheck',
contributors: 'Contributors',
license: 'License'
}
+1 -1
View File
@@ -169,7 +169,7 @@ window.localisation.es = {
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
killswitch_interval: 'Intervalo de Killswitch',
killswitch_interval_desc:
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNBits desde la fuente de estado (en minutos).',
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNbits desde la fuente de estado (en minutos).',
enable_watchdog: 'Activar Watchdog',
enable_watchdog_desc:
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
+1 -1
View File
@@ -66,7 +66,7 @@ window.localisation.fi = {
service_fee_max:
'Palvelumaksu: %{amount} % tapahtumasta (enintään %{max} sat)',
service_fee_tooltip:
'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
'LNbits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
toggle_darkmode: 'Tumma näkymä',
payment_reactions: 'Maksureaktiot',
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
+1 -1
View File
@@ -173,7 +173,7 @@ window.localisation.fr = {
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
killswitch_interval: 'Intervalle du Killswitch',
killswitch_interval_desc:
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNBits provenant de la source de statut (en minutes).",
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNbits provenant de la source de statut (en minutes).",
enable_watchdog: 'Activer le Watchdog',
enable_watchdog_desc:
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
+1 -1
View File
@@ -169,7 +169,7 @@ window.localisation.it = {
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
killswitch_interval: 'Intervallo Killswitch',
killswitch_interval_desc:
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNBits dalla fonte di stato (in minuti).',
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNbits dalla fonte di stato (in minuti).',
enable_watchdog: 'Attiva Watchdog',
enable_watchdog_desc:
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
+1 -1
View File
@@ -166,7 +166,7 @@ window.localisation.jp = {
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
killswitch_interval: 'キルスイッチ間隔',
killswitch_interval_desc:
'バックグラウンドタスクがステータスソースからLNBitsキルスイッチ信号を確認する頻度(分単位)。',
'バックグラウンドタスクがステータスソースからLNbitsキルスイッチ信号を確認する頻度(分単位)。',
enable_watchdog: 'ウォッチドッグを有効にする',
enable_watchdog_desc:
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
+2 -2
View File
@@ -40,7 +40,7 @@ window.localisation.kr = {
'성공적으로 가상 자금을 생성했습니다 (%{amount} sats). 지급은 자금 원천의 실제 자금에 따라 달라집니다.',
paste_invoice_label: '인보이스, 결제 요청, 혹은 lnurl 코드를 붙여넣으세요 *',
lnbits_description:
'설정이 쉽고 가벼운 LNBits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNBits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNBits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNBits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNBits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNBits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
'설정이 쉽고 가벼운 LNbits는 어떤 라이트닝 네트워크의 예산 자원 위에서든 돌아갈 수 있습니다, 그리고 다른 LNbits 지갑들입니다. 스스로 사용하기 위해, 또는 다른 사람들에게 수탁형 솔루션을 제공하기 위해 LNbits를 운영할 수 있습니다. 각 지갑들은 자신만의 API key를 가지며, 생성 가능한 지갑의 수에는 제한이 없습니다. 자금을 분할할 수 있는 기능으로 인해, LNbits는 자금 운영 도구로써뿐만 아니라 개발 도구로써도 유용합니다. 확장 기능들은 LNbits에 여러분들이 라이트닝 네트워크의 다양한 최신 기술들을 수행해볼 수 있게 하는 추가 기능을 제공합니다. LNbits 개발진들은 확장 기능들의 개발 또한 가능한 쉽게 만들었으며, 무료 오픈 소스 프로젝트답게 사람들이 자신만의 확장 기능들을 개발하고 제출하기를 응원합니다.',
export_to_phone: 'QR 코드를 이용해 모바일 기기로 내보내기',
export_to_phone_desc:
'이 QR 코드는 선택된 지갑의 최대 접근 권한을 가진 전체 URL을 담고 있습니다. 스캔 후, 모바일 기기에서 지갑을 열 수 있습니다.',
@@ -62,7 +62,7 @@ window.localisation.kr = {
service_fee: '서비스 수수료: 거래액의 %{amount} %',
service_fee_max: '서비스 수수료: 거래액의 %{amount} % (최대 %{max} sats)',
service_fee_tooltip:
'지불 결제 시마다 LNBits 서버 관리자에게 납부되는 서비스 수수료',
'지불 결제 시마다 LNbits 서버 관리자에게 납부되는 서비스 수수료',
toggle_darkmode: '다크 모드 전환',
payment_reactions: '결제 반응',
view_swagger_docs: 'LNbits Swagger API 문서를 봅니다',
+1 -1
View File
@@ -169,7 +169,7 @@ window.localisation.nl = {
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
killswitch_interval: 'Uitschakelschakelaar-interval',
killswitch_interval_desc:
'Hoe vaak de achtergrondtaak moet controleren op het LNBits killswitch signaal van de statusbron (in minuten).',
'Hoe vaak de achtergrondtaak moet controleren op het LNbits killswitch signaal van de statusbron (in minuten).',
enable_watchdog: 'Inschakelen Watchdog',
enable_watchdog_desc:
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
+1 -1
View File
@@ -168,7 +168,7 @@ window.localisation.pi = {
"If enabled it'll be changin' yer fundin' source to VoidWallet automatically if LNbits sends out a killswitch signal, ye will. Ye'll be needin' t' enable manually after an update, arr.",
killswitch_interval: 'Killswitch Interval',
killswitch_interval_desc:
"How oft th' background task should be checkin' fer th' LNBits killswitch signal from th' status source (in minutes).",
"How oft th' background task should be checkin' fer th' LNbits killswitch signal from th' status source (in minutes).",
enable_watchdog: 'Enable Seadog',
enable_watchdog_desc:
"If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.",
+1 -1
View File
@@ -166,7 +166,7 @@ window.localisation.pl = {
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli LNbits wyśle sygnał wyłączający. Po aktualizacji będziesz musiał włączyć to ręcznie.',
killswitch_interval: 'Interwał wyłącznika awaryjnego',
killswitch_interval_desc:
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNBits ze źródła statusu (w minutach).',
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNbits ze źródła statusu (w minutach).',
enable_watchdog: 'Włącz Watchdog',
enable_watchdog_desc:
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.',
+1 -1
View File
@@ -168,7 +168,7 @@ window.localisation.pt = {
'Se ativado, ele mudará sua fonte de financiamento para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
killswitch_interval: 'Intervalo do Killswitch',
killswitch_interval_desc:
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNBits proveniente da fonte de status (em minutos).',
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc:
'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.',
+1 -1
View File
@@ -166,7 +166,7 @@ window.localisation.we = {
'Os bydd yn galluogi, bydd yn newid eich ffynhonnell arian i VoidWallet yn awtomatig os bydd LNbits yn anfon arwydd killswitch. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
killswitch_interval: 'Amlder Cyllell Dorri',
killswitch_interval_desc:
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNBits o'r ffynhonnell statws (mewn munudau).",
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNbits o'r ffynhonnell statws (mewn munudau).",
enable_watchdog: 'Galluogi Watchdog',
enable_watchdog_desc:
'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
@@ -0,0 +1,16 @@
Vue.component('lnbits-extension-rating', {
name: 'lnbits-extension-rating',
props: ['rating'],
template: `
<div style="margin-bottom: 3px">
<q-rating
v-model="rating"
size="1.5em"
:max="5"
color="primary"
><q-tooltip>
<span v-text="$t('extension_rating_soon')"></span> </q-tooltip
></q-rating>
</div>
`
})
@@ -1,6 +1,14 @@
Vue.component('lnbits-funding-sources', {
mixins: [windowMixin],
props: ['form-data', 'allowed-funding-sources'],
methods: {
getFundingSourceLabel(item) {
const fundingSource = this.rawFundingSources.find(
fundingSource => fundingSource[0] === item
)
return fundingSource ? fundingSource[1] : item
}
},
computed: {
fundingSources() {
let tmp = []
@@ -14,6 +22,9 @@ Vue.component('lnbits-funding-sources', {
tmp.push([key, tmpObj])
}
return new Map(tmp)
},
sortedAllowedFundingSources() {
return this.allowedFundingSources.sort()
}
},
data() {
@@ -93,12 +104,21 @@ Vue.component('lnbits-funding-sources', {
],
[
'LNbitsWallet',
'LNBits',
'LNbits',
{
lnbits_endpoint: 'Endpoint',
lnbits_key: 'Admin Key'
}
],
[
'BlinkWallet',
'Blink',
{
blink_api_endpoint: 'Endpoint',
blink_ws_endpoint: 'WebSocket',
blink_token: 'Key'
}
],
[
'AlbyWallet',
'Alby',
@@ -159,7 +179,8 @@ Vue.component('lnbits-funding-sources', {
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:options="allowedFundingSources"
:options="sortedAllowedFundingSources"
:option-label="(item) => getFundingSourceLabel(item)"
></q-select>
</div>
</div>
+1 -1
View File
@@ -542,7 +542,7 @@ new Vue({
pasteToTextArea: function () {
this.$refs.textArea.focus() // Set cursor to textarea
navigator.clipboard.readText().then(text => {
this.$refs.textArea.value = text
this.parse.data.request = text.trim()
})
}
},
+7
View File
@@ -231,3 +231,10 @@ video {
.whitespace-pre-line {
white-space: pre-line;
}
.q-carousel__slide {
background-size: contain;
background-repeat: no-repeat;
}
.q-dialog__inner--minimized {
padding: 12px;
}
+1
View File
@@ -37,6 +37,7 @@
"js/components.js",
"js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js",
"js/components/extension-rating.js",
"js/components/payment-list.js",
"js/components/payment-chart.js",
"js/event-reactions.js",
+2 -2
View File
@@ -175,7 +175,7 @@ async def check_pending_payments():
async def invoice_callback_dispatcher(checking_id: str):
"""
Takes incoming payments, sets pending=False, and dispatches them to
Takes an incoming payment, checks its status, and dispatches it to
invoice_listeners from core and extensions.
"""
payment = await get_standalone_payment(checking_id, incoming=True)
@@ -183,7 +183,7 @@ async def invoice_callback_dispatcher(checking_id: str):
logger.trace(
f"invoice listeners: sending invoice callback for payment {checking_id}"
)
await payment.set_pending(False)
await payment.check_status()
for name, send_chan in invoice_listeners.items():
logger.trace(f"invoice listeners: sending to `{name}`")
await send_chan.put(payment)
+1
View File
@@ -8,6 +8,7 @@ from lnbits.settings import settings
from lnbits.wallets.base import Wallet
from .alby import AlbyWallet
from .blink import BlinkWallet
from .cliche import ClicheWallet
from .corelightning import CoreLightningWallet
+2
View File
@@ -137,6 +137,8 @@ class Wallet(ABC):
def normalize_endpoint(self, endpoint: str, add_proto=True) -> str:
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
if add_proto:
if endpoint.startswith("ws://") or endpoint.startswith("wss://"):
return endpoint
endpoint = (
f"https://{endpoint}" if not endpoint.startswith("http") else endpoint
)
+483
View File
@@ -0,0 +1,483 @@
import asyncio
import hashlib
import json
from typing import AsyncGenerator, Optional
import httpx
from loguru import logger
from pydantic import BaseModel
from websockets.client import WebSocketClientProtocol, connect
from websockets.typing import Subprotocol
from lnbits import bolt11
from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentResponse,
PaymentStatus,
StatusResponse,
Wallet,
)
class BlinkWallet(Wallet):
"""https://dev.blink.sv/"""
def __init__(self):
if not settings.blink_api_endpoint:
raise ValueError(
"cannot initialize BlinkWallet: missing blink_api_endpoint"
)
if not settings.blink_ws_endpoint:
raise ValueError("cannot initialize BlinkWallet: missing blink_ws_endpoint")
if not settings.blink_token:
raise ValueError("cannot initialize BlinkWallet: missing blink_token")
self.endpoint = self.normalize_endpoint(settings.blink_api_endpoint)
self.auth = {
"X-API-KEY": settings.blink_token,
"User-Agent": settings.user_agent,
}
self.ws_endpoint = self.normalize_endpoint(settings.blink_ws_endpoint)
self.ws_auth = {
"type": "connection_init",
"payload": {"X-API-KEY": settings.blink_token},
}
self.client = httpx.AsyncClient(base_url=self.endpoint, headers=self.auth)
self.ws: Optional[WebSocketClientProtocol] = None
self._wallet_id = None
@property
def wallet_id(self):
if self._wallet_id:
return self._wallet_id
raise ValueError("Wallet id not initialized.")
async def cleanup(self):
try:
await self.client.aclose()
except RuntimeError as e:
logger.warning(f"Error closing wallet connection: {e}")
try:
if self.ws:
await self.ws.close(reason="Shutting down.")
except RuntimeError as e:
logger.warning(f"Error closing websocket connection: {e}")
async def status(self) -> StatusResponse:
try:
await self._init_wallet_id()
payload = {"query": q.balance_query, "variables": {}}
response = await self._graphql_query(payload)
wallets = (
response.get("data", {})
.get("me", {})
.get("defaultAccount", {})
.get("wallets", [])
)
btc_balance = next(
(
wallet["balance"]
for wallet in wallets
if wallet["walletCurrency"] == "BTC"
),
None,
)
if btc_balance is None:
return StatusResponse("No BTC balance", 0)
# multiply balance by 1000 to get msats balance
return StatusResponse(None, btc_balance * 1000)
except ValueError as exc:
return StatusResponse(str(exc), 0)
except Exception as exc:
logger.warning(exc)
return StatusResponse(f"Unable to connect, got: '{exc}'", 0)
async def create_invoice(
self,
amount: int,
memo: Optional[str] = None,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
# https://dev.blink.sv/api/btc-ln-receive
invoice_variables = {
"input": {
"amount": amount,
"recipientWalletId": self.wallet_id,
}
}
if description_hash:
invoice_variables["input"]["descriptionHash"] = description_hash.hex()
elif unhashed_description:
invoice_variables["input"]["descriptionHash"] = hashlib.sha256(
unhashed_description
).hexdigest()
else:
invoice_variables["input"]["memo"] = memo or ""
data = {"query": q.invoice_query, "variables": invoice_variables}
try:
response = await self._graphql_query(data)
errors = (
response.get("data", {})
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
.get("errors", {})
)
if len(errors) > 0:
error_message = errors[0].get("message")
return InvoiceResponse(False, None, None, error_message)
payment_request = (
response.get("data", {})
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
.get("invoice", {})
.get("paymentRequest", None)
)
checking_id = (
response.get("data", {})
.get("lnInvoiceCreateOnBehalfOfRecipient", {})
.get("invoice", {})
.get("paymentHash", None)
)
return InvoiceResponse(True, checking_id, payment_request, None)
except json.JSONDecodeError:
return InvoiceResponse(
False, None, None, "Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(exc)
return InvoiceResponse(
False, None, None, f"Unable to connect to {self.endpoint}."
)
async def pay_invoice(
self, bolt11_invoice: str, fee_limit_msat: int
) -> PaymentResponse:
# https://dev.blink.sv/api/btc-ln-send
# Future: add check fee estimate is < fee_limit_msat before paying invoice
payment_variables = {
"input": {
"paymentRequest": bolt11_invoice,
"walletId": self.wallet_id,
"memo": "Payment memo",
}
}
data = {"query": q.payment_query, "variables": payment_variables}
try:
response = await self._graphql_query(data)
errors = (
response.get("data", {})
.get("lnInvoicePaymentSend", {})
.get("errors", {})
)
if len(errors) > 0:
error_message = errors[0].get("message")
return PaymentResponse(False, None, None, None, error_message)
checking_id = bolt11.decode(bolt11_invoice).payment_hash
payment_status = await self.get_payment_status(checking_id)
fee_msat = payment_status.fee_msat
preimage = payment_status.preimage
return PaymentResponse(True, checking_id, fee_msat, preimage, None)
except Exception as exc:
logger.info(f"Failed to pay invoice {bolt11_invoice}")
logger.warning(exc)
return PaymentResponse(
None, None, None, None, f"Unable to connect to {self.endpoint}."
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
statuses = {
"EXPIRED": False,
"PENDING": None,
"PAID": True,
}
variables = {"paymentHash": checking_id, "walletId": self.wallet_id}
data = {"query": q.status_query, "variables": variables}
try:
response = await self._graphql_query(data)
if response.get("errors") is not None:
logger.trace(response.get("errors"))
return PaymentStatus(None)
status = response["data"]["me"]["defaultAccount"]["walletById"][
"invoiceByPaymentHash"
]["paymentStatus"]
return PaymentStatus(statuses[status])
except Exception as e:
logger.warning(f"Error getting invoice status: {e}")
return PaymentStatus(None)
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
variables = {
"walletId": self.wallet_id,
"transactionsByPaymentHash": checking_id,
}
data = {"query": q.tx_query, "variables": variables}
statuses = {
"FAILURE": False,
"EXPIRED": False,
"PENDING": None,
"PAID": True,
"SUCCESS": True,
}
try:
response = await self._graphql_query(data)
response_data = response.get("data")
assert response_data is not None
txs_data = (
response_data.get("me", {})
.get("defaultAccount", {})
.get("walletById", {})
.get("transactionsByPaymentHash", [])
)
tx_data = next((t for t in txs_data if t.get("direction") == "SEND"), None)
assert tx_data, "No SEND data found."
fee = tx_data.get("settlementFee")
preimage = tx_data.get("settlementVia", {}).get("preImage")
status = tx_data.get("status")
return PaymentStatus(
paid=statuses[status], fee_msat=fee * 1000, preimage=preimage
)
except Exception as e:
logger.error(f"Error getting payment status: {e}")
return PaymentStatus(None)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
subscription_id = "blink_payment_stream"
while settings.lnbits_running:
try:
async with connect(
self.ws_endpoint, subprotocols=[Subprotocol("graphql-transport-ws")]
) as ws:
logger.info("Connected to blink invoices stream.")
self.ws = ws
await ws.send(json.dumps(self.ws_auth))
confirmation = await ws.recv()
ack = json.loads(confirmation)
assert (
ack.get("type") == "connection_ack"
), "Websocket connection not acknowledged."
logger.info("Websocket connection acknowledged.")
subscription_req = {
"id": subscription_id,
"type": "subscribe",
"payload": {"query": q.my_updates_query, "variables": {}},
}
await ws.send(json.dumps(subscription_req))
while settings.lnbits_running:
message = await ws.recv()
resp = json.loads(message)
if resp.get("id") != subscription_id:
continue
tx = (
resp.get("payload", {})
.get("data", {})
.get("myUpdates", {})
.get("update", {})
.get("transaction", {})
)
if tx.get("direction") != "RECEIVE":
continue
if not tx.get("initiationVia"):
continue
payment_hash = tx.get("initiationVia").get("paymentHash")
if payment_hash:
yield payment_hash
except Exception as exc:
logger.error(
f"lost connection to blink invoices stream: '{exc}'"
"retrying in 5 seconds"
)
await asyncio.sleep(5)
async def _graphql_query(self, payload) -> dict:
response = await self.client.post(self.endpoint, json=payload, timeout=10)
response.raise_for_status()
return response.json()
async def _init_wallet_id(self) -> str:
"""
Get the defaultAccount wallet id, required for payments.
"""
if self._wallet_id:
return self._wallet_id
try:
payload = {
"query": q.wallet_query,
"variables": {},
}
response = await self._graphql_query(payload)
wallets = (
response.get("data", {})
.get("me", {})
.get("defaultAccount", {})
.get("wallets", [])
)
btc_wallet_ids = [
wallet["id"] for wallet in wallets if wallet["walletCurrency"] == "BTC"
]
if not btc_wallet_ids:
raise ValueError("BTC Wallet not found")
self._wallet_id = btc_wallet_ids[0]
return self._wallet_id
except Exception as exc:
logger.warning(exc)
raise ValueError(f"Unable to connect to '{self.endpoint}'") from exc
class BlinkGrafqlQueries(BaseModel):
balance_query: str
invoice_query: str
payment_query: str
status_query: str
wallet_query: str
tx_query: str
my_updates_query: str
q = BlinkGrafqlQueries(
balance_query="""
query Me {
me {
defaultAccount {
wallets {
walletCurrency
balance
}
}
}
}
""",
invoice_query="""
mutation LnInvoiceCreateOnBehalfOfRecipient(
$input: LnInvoiceCreateOnBehalfOfRecipientInput!
) {
lnInvoiceCreateOnBehalfOfRecipient(input: $input) {
invoice {
paymentRequest
paymentHash
paymentSecret
satoshis
}
errors {
message
}
}
}
""",
payment_query="""
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
lnInvoicePaymentSend(input: $input) {
status
errors {
message
path
code
}
}
}
""",
status_query="""
query InvoiceByPaymentHash($walletId: WalletId!, $paymentHash: PaymentHash!) {
me {
defaultAccount {
walletById(walletId: $walletId) {
invoiceByPaymentHash(paymentHash: $paymentHash) {
... on LnInvoice {
paymentStatus
}
}
}
}
}
}
""",
wallet_query="""
query me {
me {
defaultAccount {
wallets {
id
walletCurrency
}
}
}
}
""",
tx_query="""
query TransactionsByPaymentHash(
$walletId: WalletId!
$transactionsByPaymentHash: PaymentHash!
) {
me {
defaultAccount {
walletById(walletId: $walletId) {
walletCurrency
... on BTCWallet {
transactionsByPaymentHash(paymentHash: $transactionsByPaymentHash) {
settlementFee
status
direction
settlementVia {
... on SettlementViaLn {
preImage
}
}
}
}
}
}
}
}
""",
my_updates_query="""
subscription {
myUpdates {
update {
... on LnUpdate {
transaction {
initiationVia {
... on InitiationViaLn {
paymentHash
}
}
direction
}
}
}
}
}
""",
)
+15 -4
View File
@@ -1,5 +1,6 @@
import asyncio
import base64
import hashlib
import json
import urllib.parse
from typing import AsyncGenerator, Dict, Optional
@@ -17,7 +18,6 @@ from .base import (
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
UnsupportedError,
Wallet,
)
@@ -87,17 +87,28 @@ class PhoenixdWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
if description_hash or unhashed_description:
raise UnsupportedError("description_hash")
try:
msats_amount = amount
data: Dict = {
"amountSat": f"{msats_amount}",
"description": memo,
"externalId": "",
}
# Either 'description' (string) or 'descriptionHash' must be supplied
# PhoenixD description limited to 128 characters
if description_hash:
data["descriptionHash"] = description_hash.hex()
else:
desc = memo
if desc is None and unhashed_description:
desc = unhashed_description.decode()
desc = desc or ""
if len(desc) > 128:
data["descriptionHash"] = hashlib.sha256(desc.encode()).hexdigest()
else:
data["description"] = desc
r = await self.client.post(
"/createinvoice",
data=data,
+7 -7
View File
@@ -168,12 +168,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -434,9 +434,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
+1
View File
@@ -86,6 +86,7 @@
"js/components.js",
"js/components/lnbits-funding-sources.js",
"js/components/extension-settings.js",
"js/components/extension-rating.js",
"js/components/payment-list.js",
"js/components/payment-chart.js",
"js/event-reactions.js",
Generated
+93 -105
View File
@@ -331,13 +331,13 @@ secp256k1 = "*"
[[package]]
name = "certifi"
version = "2023.11.17"
version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
@@ -747,22 +747,23 @@ files = [
[[package]]
name = "dnspython"
version = "2.4.2"
version = "2.6.1"
description = "DNS toolkit"
optional = false
python-versions = ">=3.8,<4.0"
python-versions = ">=3.8"
files = [
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"},
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"},
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
]
[package.extras]
dnssec = ["cryptography (>=2.6,<42.0)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"]
doq = ["aioquic (>=0.9.20)"]
idna = ["idna (>=2.1,<4.0)"]
trio = ["trio (>=0.14,<0.23)"]
wmi = ["wmi (>=1.5.1,<2.0.0)"]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=41)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=0.9.25)"]
idna = ["idna (>=3.6)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "ecdsa"
@@ -1044,13 +1045,13 @@ license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.6"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
@@ -1095,13 +1096,13 @@ files = [
[[package]]
name = "jinja2"
version = "3.1.3"
version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
@@ -1311,16 +1312,6 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -1814,47 +1805,54 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.9"
version = "1.10.17"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"},
{file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"},
{file = "pydantic-1.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896"},
{file = "pydantic-1.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d"},
{file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f"},
{file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4"},
{file = "pydantic-1.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f"},
{file = "pydantic-1.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0"},
{file = "pydantic-1.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7"},
{file = "pydantic-1.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d"},
{file = "pydantic-1.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c"},
{file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91"},
{file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8"},
{file = "pydantic-1.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f"},
{file = "pydantic-1.10.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece"},
{file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a"},
{file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a"},
{file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60"},
{file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf"},
{file = "pydantic-1.10.9-cp37-cp37m-win_amd64.whl", hash = "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29"},
{file = "pydantic-1.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82"},
{file = "pydantic-1.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6"},
{file = "pydantic-1.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766"},
{file = "pydantic-1.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3"},
{file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572"},
{file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e"},
{file = "pydantic-1.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb"},
{file = "pydantic-1.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298"},
{file = "pydantic-1.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276"},
{file = "pydantic-1.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60"},
{file = "pydantic-1.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc"},
{file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a"},
{file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4"},
{file = "pydantic-1.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1"},
{file = "pydantic-1.10.9-py3-none-any.whl", hash = "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305"},
{file = "pydantic-1.10.9.tar.gz", hash = "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be"},
{file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
{file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
{file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
{file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
{file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
{file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
{file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
{file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
{file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
{file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
{file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
]
[package.dependencies]
@@ -1893,13 +1891,13 @@ pyln-proto = ">=23"
[[package]]
name = "pyln-proto"
version = "23.11"
version = "24.5"
description = "This package implements some of the Lightning Network protocol in pure python. It is intended for protocol testing and some minor tooling only. It is not deemed secure enough to handle any amount of real funds (you have been warned!)."
optional = false
python-versions = ">=3.8,<4.0"
python-versions = "<4.0,>=3.8"
files = [
{file = "pyln_proto-23.11-py3-none-any.whl", hash = "sha256:7f3e9df5fac242db4e27b5879e1df2bd318ec8f7b125132ea7a60ce17340bded"},
{file = "pyln_proto-23.11.tar.gz", hash = "sha256:e7056386be1527fd2c49a3db9228a9f5fd44cd5cdc9b1d431b21112137dd5957"},
{file = "pyln_proto-24.5-py3-none-any.whl", hash = "sha256:78b9982412cfea8172dfafe217001bdf3d6ee8d585f138b8868925e47a8c54b2"},
{file = "pyln_proto-24.5.tar.gz", hash = "sha256:b6e7a0cd5bd3905d743ec2c3ef93233271062e0abf0b7647a449ff71c0f20074"},
]
[package.dependencies]
@@ -2134,7 +2132,6 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -2142,15 +2139,8 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -2167,7 +2157,6 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -2175,7 +2164,6 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -2215,13 +2203,13 @@ test = ["ipython", "mock", "pytest (>=3.0.5)"]
[[package]]
name = "requests"
version = "2.31.0"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
@@ -2431,19 +2419,18 @@ cffi = ">=1.3.0"
[[package]]
name = "setuptools"
version = "69.0.2"
version = "70.3.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"},
{file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"},
{file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"},
{file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "shortuuid"
@@ -2616,13 +2603,13 @@ files = [
[[package]]
name = "tqdm"
version = "4.66.2"
version = "4.66.4"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
{file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
{file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
{file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
{file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
]
[package.dependencies]
@@ -2705,17 +2692,18 @@ files = [
[[package]]
name = "urllib3"
version = "2.1.0"
version = "2.2.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
{file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
{file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
@@ -2937,13 +2925,13 @@ files = [
[[package]]
name = "werkzeug"
version = "3.0.2"
version = "3.0.3"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.8"
files = [
{file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"},
{file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"},
{file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
{file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
]
[package.dependencies]
@@ -3047,18 +3035,18 @@ files = [
[[package]]
name = "zipp"
version = "3.17.0"
version = "3.19.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[extras]
liquid = ["wallycore"]
@@ -3066,4 +3054,4 @@ liquid = ["wallycore"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10 | ^3.9"
content-hash = "4147363e9d238b0d511d5e4fe9996debea068ee58e85477ac7e4515644c60e71"
content-hash = "33f9d6ee851ae77b6e02cc8964d1a6ea233ba3ff4cfaeeb082c327654c9cd7e0"
+3 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lnbits"
version = "0.12.8"
version = "0.12.10"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
readme = "README.md"
@@ -18,10 +18,10 @@ click = "8.1.7"
ecdsa = "0.18.0"
fastapi = "0.109.2"
httpx = "0.25.0"
jinja2 = "3.1.3"
jinja2 = "3.1.4"
lnurl = "0.4.2"
psycopg2-binary = "2.9.7"
pydantic = "1.10.9"
pydantic = "1.10.17"
pyqrcode = "1.2.1"
shortuuid = "1.0.11"
sqlalchemy = "1.3.24"
+148
View File
@@ -0,0 +1,148 @@
import os
import pytest
from loguru import logger
from lnbits.settings import settings
from lnbits.wallets import BlinkWallet, get_funding_source, set_funding_source
settings.lnbits_backend_wallet_class = "BlinkWallet"
settings.blink_token = "mock"
settings.blink_api_endpoint = "https://api.blink.sv/graphql"
# Check if BLINK_TOKEN environment variable is set
use_real_api = os.environ.get("BLINK_TOKEN") is not None
logger.info(f"use_real_api: {use_real_api}")
if use_real_api:
headers = {
"Content-Type": "application/json",
"X-API-KEY": os.environ.get("BLINK_TOKEN"),
}
settings.blink_token = os.environ.get("BLINK_TOKEN")
logger.info(
f"settings.lnbits_backend_wallet_class: {settings.lnbits_backend_wallet_class}"
)
logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
logger.info(f"settings.blink_token: {settings.blink_token}")
set_funding_source()
funding_source = get_funding_source()
assert isinstance(funding_source, BlinkWallet)
@pytest.fixture(scope="session")
def payhash():
# put your external payment hash here
payment_hash = "14d7899c3456bcd78f7f18a70d782b8eadb2de974e80dc5120e133032423dcda"
return payment_hash
@pytest.fixture(scope="session")
def outbound_bolt11():
# put your outbound bolt11 here
bolt11 = "lnbc1u1pjl0uhypp5yxvdqq923atm9ywkpgtu3yxv9w2n44ensrkwfyagvmzqhml2x9gqdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsssp5h3qlnnlfqekquacwwj9yu7fhujyzxhzqegpxenscw45pgv6xakfq9qyyssqqjruygw0jrcg3365jksxn6yhsxx7c5pdjrjdlyvuhs7xh8r409h4e3kucc54kgh34pscaq3mg7hn55l8a0qszgzex80amwrp4gkdgqcpkse88y" # noqa: E501
return bolt11
@pytest.mark.asyncio
async def test_environment_variables():
if use_real_api:
assert "X-API-KEY" in headers, "X-API-KEY is not present in headers"
assert isinstance(headers["X-API-KEY"], str), "X-API-KEY is not a string"
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_get_wallet_id():
if use_real_api:
wallet_id = await funding_source._init_wallet_id()
logger.info(f"test_get_wallet_id: {wallet_id}")
assert wallet_id
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_status():
if use_real_api:
status = await funding_source.status()
logger.info(f"test_status: {status}")
assert status
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_create_invoice():
if use_real_api:
invoice_response = await funding_source.create_invoice(amount=1000, memo="test")
assert invoice_response.ok is True
assert invoice_response.payment_request
assert invoice_response.checking_id
logger.info(f"test_create_invoice: ok: {invoice_response.ok}")
logger.info(
f"test_create_invoice: payment_request: {invoice_response.payment_request}"
)
payment_status = await funding_source.get_invoice_status(
invoice_response.checking_id
)
assert payment_status.paid is None # still pending
logger.info(
f"test_create_invoice: PaymentStatus is Still Pending: {payment_status.paid is None}" # noqa: E501
)
logger.info(
f"test_create_invoice: PaymentStatusfee_msat: {payment_status.fee_msat}"
)
logger.info(
f"test_create_invoice: PaymentStatus preimage: {payment_status.preimage}"
)
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_pay_invoice_self_payment():
if use_real_api:
invoice_response = await funding_source.create_invoice(amount=100, memo="test")
assert invoice_response.ok is True
bolt11 = invoice_response.payment_request
assert bolt11 is not None
payment_response = await funding_source.pay_invoice(bolt11, fee_limit_msat=100)
assert payment_response.ok is False # can't pay self
assert payment_response.error_message
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_outbound_invoice_payment(outbound_bolt11):
if use_real_api:
payment_response = await funding_source.pay_invoice(
outbound_bolt11, fee_limit_msat=100
)
assert payment_response.ok is True
assert payment_response.checking_id
logger.info(f"test_outbound_invoice_payment: ok: {payment_response.ok}")
logger.info(
f"test_outbound_invoice_payment: checking_id: {payment_response.checking_id}" # noqa: E501
)
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"
@pytest.mark.asyncio
async def test_get_payment_status(payhash):
if use_real_api:
payment_status = await funding_source.get_payment_status(payhash)
assert payment_status.paid
logger.info(f"test_get_payment_status: payment_status: {payment_status.paid}")
else:
assert True, "BLINK_TOKEN is not set. Skipping test using mock api"