Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11173e6460 | ||
|
|
069a7922fc | ||
|
|
98338ffb00 | ||
|
|
62dec118af | ||
|
|
84b406534d | ||
|
|
d9d2d59b73 | ||
|
|
20d4b954c0 | ||
|
|
5edb845d8f | ||
|
|
cfe2b43d2a | ||
|
|
a0ac41a9ce | ||
|
|
194a527736 | ||
|
|
6f135c0696 | ||
|
|
9a767b627f | ||
|
|
c1c4eda69d | ||
|
|
6c46867698 | ||
|
|
a73eb569ae |
+30
-21
@@ -1,5 +1,28 @@
|
||||
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
|
||||
|
||||
######################################
|
||||
########### Admin Settings ###########
|
||||
######################################
|
||||
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
|
||||
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the
|
||||
# configurations defined in `ReadOnlySettings` will still be read from the environment variables.
|
||||
# The rest of the settings will be stored in your database and you will be able to change them
|
||||
# only through the Admin UI.
|
||||
# Disable this to make LNbits use this config file again.
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
|
||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
|
||||
######################################
|
||||
########## Funding Source ############
|
||||
######################################
|
||||
@@ -78,27 +101,6 @@ ECLAIR_PASS=eclairpw
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
||||
######################################
|
||||
########### Admin Settings ###########
|
||||
######################################
|
||||
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||
# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will
|
||||
# be stored in your database and you will be able to change them only through the Admin UI.
|
||||
# Disable this to make LNbits use this config file again.
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Change theme
|
||||
LNBITS_SITE_TITLE="LNbits"
|
||||
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
|
||||
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
|
||||
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
|
||||
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
|
||||
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
|
||||
|
||||
HOST=127.0.0.1
|
||||
PORT=5000
|
||||
|
||||
######################################
|
||||
####### Auth Configurations ##########
|
||||
######################################
|
||||
@@ -198,6 +200,13 @@ LNBITS_RESERVE_FEE_MIN=2000
|
||||
# value in percent
|
||||
LNBITS_RESERVE_FEE_PERCENT=1.0
|
||||
|
||||
# limit the maximum balance for each wallet
|
||||
# throw an error if the wallet attempts to create a new invoice
|
||||
|
||||
# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000
|
||||
# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000
|
||||
# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60
|
||||
|
||||
# Limit fiat currencies allowed to see in UI
|
||||
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
default: latest
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
@@ -38,7 +43,7 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }}
|
||||
platforms: [ linux/amd64, linux/arm64 ]
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
@@ -48,6 +53,6 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest
|
||||
platforms: [ linux/amd64, linux/arm64 ]
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
@@ -33,8 +33,8 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: lnbitsdocker/lnbits-legend:latest
|
||||
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest
|
||||
tags: lnbits/lnbits:latest
|
||||
cache-from: type=registry,ref=lnbits/lnbits:latest
|
||||
cache-to: type=inline
|
||||
|
||||
- name: Setup Regtest
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Create fake admin
|
||||
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
|
||||
run: docker exec lnbits-legend-lnbits-1 poetry run python tools/create_fake_admin.py
|
||||
run: docker exec lnbits-lnbits-1 poetry run python tools/create_fake_admin.py
|
||||
|
||||
- name: Run Tests
|
||||
env:
|
||||
|
||||
@@ -13,6 +13,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Create github release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -101,19 +101,19 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
||||
|
||||
use latest version from docker hub
|
||||
```sh
|
||||
docker pull lnbitsdocker/lnbits-legend
|
||||
docker pull lnbits/lnbits
|
||||
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
|
||||
mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
```
|
||||
build the image yourself
|
||||
```sh
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits
|
||||
docker build -t lnbitsdocker/lnbits-legend .
|
||||
docker build -t lnbits/lnbits .
|
||||
cp .env.example .env
|
||||
mkdir data
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
|
||||
```
|
||||
|
||||
## Option 4: Fly.io
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ from slowapi.util import get_remote_address
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lnbits.core.crud import get_installed_extensions
|
||||
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.services import websocketUpdater
|
||||
from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog,
|
||||
@@ -35,7 +35,7 @@ from lnbits.tasks import cancel_all_tasks, create_permanent_task
|
||||
from lnbits.utils.cache import cache
|
||||
from lnbits.wallets import get_wallet_class, set_wallet_class
|
||||
|
||||
from .commands import db_versions, migrate_databases
|
||||
from .commands import migrate_databases
|
||||
from .core import init_core_routers
|
||||
from .core.db import core_app_extra
|
||||
from .core.services import check_admin_settings, check_webpush_settings
|
||||
@@ -273,7 +273,7 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
||||
extension = Extension.from_installable_ext(ext)
|
||||
register_ext_routes(app, extension)
|
||||
|
||||
current_version = (await db_versions()).get(ext.id, 0)
|
||||
current_version = (await get_dbversions()).get(ext.id, 0)
|
||||
await migrate_extension_database(extension, current_version)
|
||||
|
||||
# mount routes for the new version
|
||||
|
||||
+67
-2
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
@@ -41,6 +42,7 @@ from .crud import (
|
||||
create_wallet,
|
||||
delete_wallet_payment,
|
||||
get_account,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_super_settings,
|
||||
get_total_balance,
|
||||
@@ -118,8 +120,9 @@ async def create_invoice(
|
||||
if not amount > 0:
|
||||
raise InvoiceFailure("Amountless invoices not supported.")
|
||||
|
||||
if await get_wallet(wallet_id, conn=conn) is None:
|
||||
raise InvoiceFailure("Wallet does not exist.")
|
||||
user_wallet = await get_wallet(wallet_id, conn=conn)
|
||||
if not user_wallet:
|
||||
raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.")
|
||||
|
||||
invoice_memo = None if description_hash else memo
|
||||
|
||||
@@ -130,6 +133,14 @@ async def create_invoice(
|
||||
amount, wallet_id, currency=currency, extra=extra, conn=conn
|
||||
)
|
||||
|
||||
if settings.is_wallet_max_balance_exceeded(
|
||||
user_wallet.balance_msat / 1000 + amount_sat
|
||||
):
|
||||
raise InvoiceFailure(
|
||||
f"Wallet balance cannot exceed "
|
||||
f"{settings.lnbits_wallet_limit_max_balance} sats."
|
||||
)
|
||||
|
||||
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
||||
amount=amount_sat,
|
||||
memo=invoice_memo,
|
||||
@@ -185,6 +196,8 @@ async def pay_invoice(
|
||||
if max_sat and invoice.amount_msat > max_sat * 1000:
|
||||
raise ValueError("Amount in invoice is too high.")
|
||||
|
||||
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
temp_id = invoice.payment_hash
|
||||
internal_id = f"internal_{invoice.payment_hash}"
|
||||
@@ -372,6 +385,58 @@ async def pay_invoice(
|
||||
return invoice.payment_hash
|
||||
|
||||
|
||||
async def check_wallet_limits(wallet_id, conn, amount_msat):
|
||||
await check_time_limit_between_transactions(conn, wallet_id)
|
||||
await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat)
|
||||
|
||||
|
||||
async def check_time_limit_between_transactions(conn, wallet_id):
|
||||
limit = settings.lnbits_wallet_limit_secs_between_trans
|
||||
if not limit or limit <= 0:
|
||||
return
|
||||
|
||||
payments = await get_payments(
|
||||
since=int(time.time()) - limit,
|
||||
wallet_id=wallet_id,
|
||||
limit=1,
|
||||
conn=conn,
|
||||
)
|
||||
|
||||
if len(payments) == 0:
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"The time limit of {limit} seconds between payments has been reached."
|
||||
)
|
||||
|
||||
|
||||
async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat):
|
||||
limit = settings.lnbits_wallet_limit_daily_max_withdraw
|
||||
if not limit or limit <= 0:
|
||||
return
|
||||
|
||||
payments = await get_payments(
|
||||
since=int(time.time()) - 60 * 60 * 24,
|
||||
outgoing=True,
|
||||
wallet_id=wallet_id,
|
||||
limit=1,
|
||||
conn=conn,
|
||||
)
|
||||
if len(payments) == 0:
|
||||
return
|
||||
|
||||
total = 0
|
||||
for pay in payments:
|
||||
total += pay.amount
|
||||
total = total - amount_msat
|
||||
if limit * 1000 + total < 0:
|
||||
raise ValueError(
|
||||
"Daily withdrawal limit of "
|
||||
+ str(settings.lnbits_wallet_limit_daily_max_withdraw)
|
||||
+ " sats reached."
|
||||
)
|
||||
|
||||
|
||||
async def redeem_lnurl_withdraw(
|
||||
wallet_id: str,
|
||||
lnurl_request: str,
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('rate_limiter')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
@@ -201,6 +202,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-12">
|
||||
<p v-text="$t('wallet_limiter')"></p>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_wallet_limit_max_balance"
|
||||
:label="$t('wallet_max_ballance')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
|
||||
:label="$t('wallet_limit_max_withdraw_per_day')"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input
|
||||
filled
|
||||
type="number"
|
||||
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
|
||||
:label="$t('wallet_limit_secs_between_trans')"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{%raw%}
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_admin_users"
|
||||
:key="user"
|
||||
@@ -24,10 +23,9 @@
|
||||
@remove="removeAdminUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{%endraw%}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
@@ -44,7 +42,6 @@
|
||||
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
{% raw %}
|
||||
<q-chip
|
||||
v-for="user in formData.lnbits_allowed_users"
|
||||
:key="user"
|
||||
@@ -52,12 +49,29 @@
|
||||
@remove="removeAllowedUser(user)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
v-text="user"
|
||||
>
|
||||
{{ user }}
|
||||
</q-chip>
|
||||
{% endraw %}
|
||||
</div>
|
||||
<br />
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Allow creation of new users</q-item-label>
|
||||
<q-item-label caption
|
||||
>Allow creation of new users on the index page</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_allow_new_accounts"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
@@ -401,6 +401,23 @@
|
||||
></lnbits-notifications-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-4">
|
||||
<span v-text="$t('payment_reactions')"></span>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-select
|
||||
v-model="reactionChoice"
|
||||
:options="reactionOptions"
|
||||
label="Reactions"
|
||||
@input="reactionChoiceFunc"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('payment_reactions')"></span
|
||||
></q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div
|
||||
v-if="isUserAuthorized"
|
||||
class="col-12 col-md-7 col-lg-6 q-gutter-y-md"
|
||||
class="col-12 col-md-6 col-lg-6 q-gutter-y-md"
|
||||
></div>
|
||||
<div v-else class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<div v-else class="col-12 col-md-4 col-lg-4 q-gutter-y-md">
|
||||
<div class="gt-sm">
|
||||
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
|
||||
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
|
||||
<h3 class="q-my-none" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
{{SITE_TITLE}}
|
||||
</h3>
|
||||
<h5 class="q-my-md" v-if="'{{SITE_TITLE}}' == 'LNbits'">
|
||||
{{SITE_TAGLINE}}
|
||||
</h5>
|
||||
</div>
|
||||
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only" in
|
||||
LNBITS_AUTH_METHODS)%}
|
||||
@@ -33,7 +37,7 @@
|
||||
{%else%} {% endif %}
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<div class="col-12 col-md-10 col-lg-10 q-gutter-y-md">
|
||||
<q-badge v-if="isAccessTokenExpired" color="primary" rounded>
|
||||
<div class="text-h5">
|
||||
<span v-text="$t('session_has_expired')"></span>
|
||||
@@ -282,11 +286,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ads -->
|
||||
<div
|
||||
class="col-12 col-md-3 col-lg-3 gt-sm"
|
||||
v-if="'{{SITE_TITLE}}' == 'LNbits'"
|
||||
v-if="'{{SITE_TITLE}}' != 'LNbits'"
|
||||
class="col-12 col-md-5 col-lg-5 q-pt-xl"
|
||||
>
|
||||
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
|
||||
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
|
||||
<div v-html="formatDescription"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3 col-lg-3 gt-sm" v-else>
|
||||
<div class="row q-col-gutter-lg justify-center">
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn
|
||||
@@ -465,27 +474,22 @@
|
||||
<div class="col q-pl-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn flat color="secondary" class="full-width q-mb-md"
|
||||
>{{ AD_SPACE_TITLE }}</q-btn
|
||||
>
|
||||
|
||||
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||
<img
|
||||
v-if="($q.dark.isActive)"
|
||||
src="{{ AD[1] }}"
|
||||
style="max-width: 90%"
|
||||
/>
|
||||
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
|
||||
<q-btn flat color="secondary" class="full-width q-mb-md"
|
||||
>{{ AD_SPACE_TITLE }}</q-btn
|
||||
>
|
||||
|
||||
<a href="{{ AD[0] }}" class="q-ma-md">
|
||||
<img v-if="($q.dark.isActive)" src="{{ AD[1] }}" style="max-width: 90%" />
|
||||
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %} {% endif %}
|
||||
</div>
|
||||
<div class="row gt-sm q-mt-xl">
|
||||
<div v-if="'{{SITE_TITLE}}' == 'LNbits'" class="row gt-sm q-mt-xl">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-10 q-pl-xl">
|
||||
<span v-text="$t('lnbits_description')"></span>
|
||||
|
||||
@@ -17,9 +17,11 @@ from lnbits.core.services import (
|
||||
update_cached_settings,
|
||||
update_wallet_balance,
|
||||
)
|
||||
from lnbits.core.tasks import api_invoice_listeners
|
||||
from lnbits.decorators import check_admin, check_super_user
|
||||
from lnbits.server import server_restart
|
||||
from lnbits.settings import AdminSettings, UpdateSettings, settings
|
||||
from lnbits.tasks import invoice_listeners
|
||||
|
||||
from .. import core_app_extra
|
||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||
@@ -48,6 +50,19 @@ async def api_auditor():
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/admin/api/v1/monitor",
|
||||
name="Monitor",
|
||||
description="show the current listeners and other monitoring data",
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def api_monitor():
|
||||
return {
|
||||
"invoice_listeners": list(invoice_listeners.keys()),
|
||||
"api_invoice_listeners": list(api_invoice_listeners.keys()),
|
||||
}
|
||||
|
||||
|
||||
@admin_router.get("/admin/api/v1/settings/", response_model=Optional[AdminSettings])
|
||||
async def api_get_settings(
|
||||
user: User = Depends(check_admin),
|
||||
|
||||
@@ -116,6 +116,9 @@ class SecuritySettings(LNbitsSettings):
|
||||
lnbits_notifications: bool = Field(default=False)
|
||||
lnbits_killswitch: bool = Field(default=False)
|
||||
lnbits_killswitch_interval: int = Field(default=60)
|
||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
||||
lnbits_watchdog: bool = Field(default=False)
|
||||
lnbits_watchdog_interval: int = Field(default=60)
|
||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
||||
@@ -125,6 +128,13 @@ class SecuritySettings(LNbitsSettings):
|
||||
)
|
||||
)
|
||||
|
||||
def is_wallet_max_balance_exceeded(self, amount):
|
||||
return (
|
||||
self.lnbits_wallet_limit_max_balance
|
||||
and self.lnbits_wallet_limit_max_balance > 0
|
||||
and amount > self.lnbits_wallet_limit_max_balance
|
||||
)
|
||||
|
||||
|
||||
class FakeWalletFundingSource(LNbitsSettings):
|
||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||
|
||||
Vendored
+9
-9
File diff suppressed because one or more lines are too long
@@ -59,6 +59,7 @@ window.localisation.en = {
|
||||
service_fee_tooltip:
|
||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||
toggle_darkmode: 'Toggle Dark Mode',
|
||||
payment_reactions: 'Payment Reactions',
|
||||
view_swagger_docs: 'View LNbits Swagger API docs',
|
||||
api_docs: 'API docs',
|
||||
api_keys_api_docs: 'API keys and API docs',
|
||||
@@ -184,6 +185,12 @@ window.localisation.en = {
|
||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
||||
enter_ip: 'Enter IP and hit enter',
|
||||
rate_limiter: 'Rate Limiter',
|
||||
wallet_limiter: 'Wallet Limiter',
|
||||
wallet_limit_max_withdraw_per_day:
|
||||
'Max daily wallet withdrawal in sats (0 to disable)',
|
||||
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
||||
wallet_limit_secs_between_trans:
|
||||
'Min secs between transactions per wallet (0 to disable)',
|
||||
number_of_requests: 'Number of requests',
|
||||
time_unit: 'Time unit',
|
||||
minute: 'minute',
|
||||
|
||||
@@ -37,7 +37,7 @@ window.localisation.fi = {
|
||||
paste_invoice_label:
|
||||
'Liitä lasku, maksupyyntö, lnurl-koodi tai Lightning Address *',
|
||||
lnbits_description:
|
||||
'Kevyt ja helppokäyttöinen LNbits soveltuu minkä tahansa salamaverkon rahoituslähteeksi. Se tukee rahoituslähteinä LND, Core Lightning, OpenNode, Alby, LNPay ja jopa itseään! Voi käyttää itsenäisesti sitä ja helposti tarjota säilytyspalveluita. Pystyt luomaan sillä salamaverkko lompakoita ja niiden määrää ei ole rajoitettu. Jokaiselle lompakolle saat iksilölliset AP avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkaluja. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon-teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään hmahdollisimman helpoksi, ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!',
|
||||
'Kevyt ja helppokäyttöinen LNbits voi käyttää rahoituslähteinään erilaisia palveluita, kuten LND, Core Lightning, OpenNode, Alby, LNPay ja jopa itseään! Voit käyttää sitä itsenäisesti ja helposti tarjota erilaisia Lightning-palveluita. Pystyt luomaan sillä salamaverkkolompakoita eikä niiden määrää ole rajoitettu. Jokaiselle lompakolle saat yksilölliset API-avaimet. Varojen osittaminen tekee siitä erittäin kätevän varojen hallinnassa sekä myös ohjelmistokehityksen työkalun. Laajennukset lisäävät LNbits:in toiminnallisuuksia. Näinpä voit helposti testailla useita erilaisia ja viimeisimpiä salamaverkon teknologioita. Laajennuksien kehittämisen olemme pyrkineet tekemään mahdollisimman helpoksi pitämällä LNbits:in ilmaisena OpenSource-projektina. Kannustamme kaikkia kehittämään ja jakelemaan omia laajennuksia!',
|
||||
export_to_phone: 'Käytä puhelimessa lukemalla QR-koodi',
|
||||
export_to_phone_desc:
|
||||
'Tämä QR-koodi sisältää URL-osoitteen, jolla saa lompakkoosi täydet valtuudet. Voi lukea sen puhelimellasi ja avata sillä lompakkosi. Voit myös lisätä lompakkosi selaimella käytettäväksi PWA-sovellukseksi puhelimen aloitusruudulle. ',
|
||||
@@ -64,6 +64,7 @@ window.localisation.fi = {
|
||||
service_fee_tooltip:
|
||||
'LNBits palvelimen ylläpitäjä veloittaa lähtevästä maksusta palvelumaksun.',
|
||||
toggle_darkmode: 'Tumma näkymä',
|
||||
toggle_reactions: 'Käytä tapahtuma efektejä',
|
||||
view_swagger_docs: 'Näytä LNbits Swagger API-dokumentit',
|
||||
api_docs: 'API-dokumentaatio',
|
||||
api_keys_api_docs: 'API-avaimet ja -dokumentaatio',
|
||||
@@ -104,9 +105,9 @@ window.localisation.fi = {
|
||||
disclaimer_dialog:
|
||||
'Muistathan tallettaa kirjautumistietosi turvallisesta ja helposti saataville, jotta pääset jatkossakin kirjautumaan lompakkoosi! Tutustu myös Tilin asetukset -sivuun. Tämä palvelu on kokeiluvaiheessa (eli BETA), ja niinpä kukaan ei ota mitään vastuuta varojen säilymisestä tai niiden käytettävyyden takaamisesta.',
|
||||
no_transactions: 'Lompakossa ei ole yhtään tapahtumaa',
|
||||
manage: 'Hallinnoi',
|
||||
manage: 'Hallinnointi',
|
||||
extensions: 'Laajennukset',
|
||||
no_extensions: 'Yhtään laajennusta ei ole asennettuna :(',
|
||||
no_extensions: 'Laajennuksia ei ole asennettu :(',
|
||||
created: 'Luotu',
|
||||
search_extensions: 'Etsi laajennuksia',
|
||||
warning: 'Varoitus',
|
||||
|
||||
@@ -6,6 +6,12 @@ new Vue({
|
||||
user: null,
|
||||
hasUsername: false,
|
||||
showUserId: false,
|
||||
reactionOptions: [
|
||||
'None',
|
||||
'confettiBothSides',
|
||||
'confettiFireworks',
|
||||
'confettiStars'
|
||||
],
|
||||
tab: 'user',
|
||||
passwordData: {
|
||||
show: false,
|
||||
@@ -27,6 +33,9 @@ new Vue({
|
||||
this.$q.dark.toggle()
|
||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||
},
|
||||
reactionChoiceFunc: function () {
|
||||
this.$q.localStorage.set('lnbits.reactions', this.reactionChoice)
|
||||
},
|
||||
changeColor: function (newValue) {
|
||||
document.body.setAttribute('data-theme', newValue)
|
||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||
|
||||
@@ -168,7 +168,6 @@ window.LNbits = {
|
||||
let listener = ev => {
|
||||
cb(JSON.parse(ev.data))
|
||||
}
|
||||
|
||||
this.listenersCount = this.listenersCount || {[wallet.inkey]: 0}
|
||||
this.listenersCount[wallet.inkey]++
|
||||
|
||||
@@ -444,6 +443,7 @@ window.windowMixin = {
|
||||
data: function () {
|
||||
return {
|
||||
toggleSubs: true,
|
||||
reactionChoice: 'confettiBothSides',
|
||||
isUserAuthorized: false,
|
||||
g: {
|
||||
offline: !navigator.onLine,
|
||||
@@ -523,6 +523,9 @@ window.windowMixin = {
|
||||
} else {
|
||||
this.$q.dark.set(true)
|
||||
}
|
||||
this.reactionChoice =
|
||||
this.$q.localStorage.getItem('lnbits.reactions') || 'confettiBothSides'
|
||||
|
||||
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
|
||||
|
||||
let locale = this.$q.localStorage.getItem('lnbits.lang')
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
function eventReactionWebocket(event_id) {
|
||||
localUrl = ''
|
||||
reaction = localStorage.getItem('lnbits.reactions')
|
||||
if (!reaction || reaction === 'None') {
|
||||
return
|
||||
}
|
||||
if (location.protocol !== 'http:') {
|
||||
localUrl = 'wss://' + location.host + '/api/v1/ws/' + event_id
|
||||
} else {
|
||||
localUrl = 'ws://' + location.host + '/api/v1/ws/' + event_id
|
||||
}
|
||||
connection = new WebSocket(localUrl)
|
||||
connection.onmessage = function (e) {
|
||||
try {
|
||||
const parsedData = JSON.parse(e.data)
|
||||
if (parsedData.payment.amount < 0) {
|
||||
return
|
||||
}
|
||||
reaction = localStorage.getItem('lnbits.reactions')
|
||||
if (reaction) {
|
||||
window[reaction.split('|')[1]]()
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
function confettiBothSides() {
|
||||
document.getElementById('vue').disabled = true
|
||||
var end = Date.now() + 2 * 1000
|
||||
var colors = ['#FFD700', '#ffffff']
|
||||
function frame() {
|
||||
confetti({
|
||||
particleCount: 2,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: {x: 0},
|
||||
colors: colors,
|
||||
zIndex: 999999
|
||||
})
|
||||
confetti({
|
||||
particleCount: 2,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: {x: 1},
|
||||
colors: colors,
|
||||
zIndex: 999999
|
||||
})
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame)
|
||||
} else {
|
||||
document.getElementById('vue').disabled = false
|
||||
}
|
||||
}
|
||||
frame()
|
||||
}
|
||||
function confettiFireworks() {
|
||||
var duration = 3 * 1000
|
||||
var animationEnd = Date.now() + duration
|
||||
var defaults = {startVelocity: 30, spread: 360, ticks: 60, zIndex: 0}
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min
|
||||
}
|
||||
|
||||
var interval = setInterval(function () {
|
||||
var timeLeft = animationEnd - Date.now()
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval)
|
||||
}
|
||||
|
||||
var particleCount = 5 * (timeLeft / duration)
|
||||
// since particles fall down, start a bit higher than random
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: {x: randomInRange(0.1, 0.3), y: Math.random() - 0.2}
|
||||
})
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: {x: randomInRange(0.7, 0.9), y: Math.random() - 0.2}
|
||||
})
|
||||
}, 250)
|
||||
}
|
||||
|
||||
function confettiStars() {
|
||||
var defaults = {
|
||||
spread: 360,
|
||||
ticks: 50,
|
||||
gravity: 0,
|
||||
decay: 0.94,
|
||||
startVelocity: 30,
|
||||
colors: ['FFE400', 'FFBD00', 'E89400', 'FFCA6C', 'FDFFB8']
|
||||
}
|
||||
|
||||
function shoot() {
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount: 40,
|
||||
scalar: 1.2,
|
||||
shapes: ['star']
|
||||
})
|
||||
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount: 10,
|
||||
scalar: 0.75,
|
||||
shapes: ['circle']
|
||||
})
|
||||
}
|
||||
|
||||
setTimeout(shoot, 0)
|
||||
setTimeout(shoot, 100)
|
||||
setTimeout(shoot, 200)
|
||||
setTimeout(shoot, 0)
|
||||
setTimeout(shoot, 100)
|
||||
setTimeout(shoot, 200)
|
||||
}
|
||||
!(function (t, e) {
|
||||
!(function t(e, n, a, i) {
|
||||
var o = !!(
|
||||
e.Worker &&
|
||||
e.Blob &&
|
||||
e.Promise &&
|
||||
e.OffscreenCanvas &&
|
||||
e.OffscreenCanvasRenderingContext2D &&
|
||||
e.HTMLCanvasElement &&
|
||||
e.HTMLCanvasElement.prototype.transferControlToOffscreen &&
|
||||
e.URL &&
|
||||
e.URL.createObjectURL
|
||||
)
|
||||
function r() {}
|
||||
function l(t) {
|
||||
var a = n.exports.Promise,
|
||||
i = void 0 !== a ? a : e.Promise
|
||||
return 'function' == typeof i ? new i(t) : (t(r, r), null)
|
||||
}
|
||||
var c,
|
||||
s,
|
||||
u,
|
||||
d,
|
||||
f,
|
||||
h,
|
||||
g,
|
||||
m,
|
||||
b =
|
||||
((u = Math.floor(1e3 / 60)),
|
||||
(d = {}),
|
||||
(f = 0),
|
||||
'function' == typeof requestAnimationFrame &&
|
||||
'function' == typeof cancelAnimationFrame
|
||||
? ((c = function (t) {
|
||||
var e = Math.random()
|
||||
return (
|
||||
(d[e] = requestAnimationFrame(function n(a) {
|
||||
f === a || f + u - 1 < a
|
||||
? ((f = a), delete d[e], t())
|
||||
: (d[e] = requestAnimationFrame(n))
|
||||
})),
|
||||
e
|
||||
)
|
||||
}),
|
||||
(s = function (t) {
|
||||
d[t] && cancelAnimationFrame(d[t])
|
||||
}))
|
||||
: ((c = function (t) {
|
||||
return setTimeout(t, u)
|
||||
}),
|
||||
(s = function (t) {
|
||||
return clearTimeout(t)
|
||||
})),
|
||||
{frame: c, cancel: s}),
|
||||
v =
|
||||
((m = {}),
|
||||
function () {
|
||||
if (h) return h
|
||||
if (!a && o) {
|
||||
var e = [
|
||||
'var CONFETTI, SIZE = {}, module = {};',
|
||||
'(' + t.toString() + ')(this, module, true, SIZE);',
|
||||
'onmessage = function(msg) {',
|
||||
' if (msg.data.options) {',
|
||||
' CONFETTI(msg.data.options).then(function () {',
|
||||
' if (msg.data.callback) {',
|
||||
' postMessage({ callback: msg.data.callback });',
|
||||
' }',
|
||||
' });',
|
||||
' } else if (msg.data.reset) {',
|
||||
' CONFETTI.reset();',
|
||||
' } else if (msg.data.resize) {',
|
||||
' SIZE.width = msg.data.resize.width;',
|
||||
' SIZE.height = msg.data.resize.height;',
|
||||
' } else if (msg.data.canvas) {',
|
||||
' SIZE.width = msg.data.canvas.width;',
|
||||
' SIZE.height = msg.data.canvas.height;',
|
||||
' CONFETTI = module.exports.create(msg.data.canvas);',
|
||||
' }',
|
||||
'}'
|
||||
].join('\n')
|
||||
try {
|
||||
h = new Worker(URL.createObjectURL(new Blob([e])))
|
||||
} catch (t) {
|
||||
return (
|
||||
void 0 !== typeof console &&
|
||||
'function' == typeof console.warn &&
|
||||
console.warn('🎊 Could not load worker', t),
|
||||
null
|
||||
)
|
||||
}
|
||||
!(function (t) {
|
||||
function e(e, n) {
|
||||
t.postMessage({options: e || {}, callback: n})
|
||||
}
|
||||
;(t.init = function (e) {
|
||||
var n = e.transferControlToOffscreen()
|
||||
t.postMessage({canvas: n}, [n])
|
||||
}),
|
||||
(t.fire = function (n, a, i) {
|
||||
if (g) return e(n, null), g
|
||||
var o = Math.random().toString(36).slice(2)
|
||||
return (g = l(function (a) {
|
||||
function r(e) {
|
||||
e.data.callback === o &&
|
||||
(delete m[o],
|
||||
t.removeEventListener('message', r),
|
||||
(g = null),
|
||||
i(),
|
||||
a())
|
||||
}
|
||||
t.addEventListener('message', r),
|
||||
e(n, o),
|
||||
(m[o] = r.bind(null, {data: {callback: o}}))
|
||||
}))
|
||||
}),
|
||||
(t.reset = function () {
|
||||
for (var e in (t.postMessage({reset: !0}), m))
|
||||
m[e](), delete m[e]
|
||||
})
|
||||
})(h)
|
||||
}
|
||||
return h
|
||||
}),
|
||||
y = {
|
||||
particleCount: 50,
|
||||
angle: 90,
|
||||
spread: 45,
|
||||
startVelocity: 45,
|
||||
decay: 0.9,
|
||||
gravity: 1,
|
||||
drift: 0,
|
||||
ticks: 200,
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
shapes: ['square', 'circle'],
|
||||
zIndex: 100,
|
||||
colors: [
|
||||
'#26ccff',
|
||||
'#a25afd',
|
||||
'#ff5e7e',
|
||||
'#88ff5a',
|
||||
'#fcff42',
|
||||
'#ffa62d',
|
||||
'#ff36ff'
|
||||
],
|
||||
disableForReducedMotion: !1,
|
||||
scalar: 1
|
||||
}
|
||||
function p(t, e, n) {
|
||||
return (function (t, e) {
|
||||
return e ? e(t) : t
|
||||
})(t && null != t[e] ? t[e] : y[e], n)
|
||||
}
|
||||
function M(t) {
|
||||
return t < 0 ? 0 : Math.floor(t)
|
||||
}
|
||||
function w(t) {
|
||||
return parseInt(t, 16)
|
||||
}
|
||||
function x(t) {
|
||||
return t.map(C)
|
||||
}
|
||||
function C(t) {
|
||||
var e = String(t).replace(/[^0-9a-f]/gi, '')
|
||||
return (
|
||||
e.length < 6 && (e = e[0] + e[0] + e[1] + e[1] + e[2] + e[2]),
|
||||
{
|
||||
r: w(e.substring(0, 2)),
|
||||
g: w(e.substring(2, 4)),
|
||||
b: w(e.substring(4, 6))
|
||||
}
|
||||
)
|
||||
}
|
||||
function k(t) {
|
||||
;(t.width = document.documentElement.clientWidth),
|
||||
(t.height = document.documentElement.clientHeight)
|
||||
}
|
||||
function I(t) {
|
||||
var e = t.getBoundingClientRect()
|
||||
;(t.width = e.width), (t.height = e.height)
|
||||
}
|
||||
function T(t, e, n, o, r) {
|
||||
var c,
|
||||
s,
|
||||
u = e.slice(),
|
||||
d = t.getContext('2d'),
|
||||
f = l(function (e) {
|
||||
function l() {
|
||||
;(c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e()
|
||||
}
|
||||
;(c = b.frame(function e() {
|
||||
!a ||
|
||||
(o.width === i.width && o.height === i.height) ||
|
||||
((o.width = t.width = i.width), (o.height = t.height = i.height)),
|
||||
o.width ||
|
||||
o.height ||
|
||||
(n(t), (o.width = t.width), (o.height = t.height)),
|
||||
d.clearRect(0, 0, o.width, o.height),
|
||||
(u = u.filter(function (t) {
|
||||
return (function (t, e) {
|
||||
;(e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
|
||||
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
|
||||
(e.wobble += 0.1),
|
||||
(e.velocity *= e.decay),
|
||||
(e.tiltAngle += 0.1),
|
||||
(e.tiltSin = Math.sin(e.tiltAngle)),
|
||||
(e.tiltCos = Math.cos(e.tiltAngle)),
|
||||
(e.random = Math.random() + 5),
|
||||
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
|
||||
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble))
|
||||
var n = e.tick++ / e.totalTicks,
|
||||
a = e.x + e.random * e.tiltCos,
|
||||
i = e.y + e.random * e.tiltSin,
|
||||
o = e.wobbleX + e.random * e.tiltCos,
|
||||
r = e.wobbleY + e.random * e.tiltSin
|
||||
return (
|
||||
(t.fillStyle =
|
||||
'rgba(' +
|
||||
e.color.r +
|
||||
', ' +
|
||||
e.color.g +
|
||||
', ' +
|
||||
e.color.b +
|
||||
', ' +
|
||||
(1 - n) +
|
||||
')'),
|
||||
t.beginPath(),
|
||||
'circle' === e.shape
|
||||
? t.ellipse
|
||||
? t.ellipse(
|
||||
e.x,
|
||||
e.y,
|
||||
Math.abs(o - a) * e.ovalScalar,
|
||||
Math.abs(r - i) * e.ovalScalar,
|
||||
(Math.PI / 10) * e.wobble,
|
||||
0,
|
||||
2 * Math.PI
|
||||
)
|
||||
: (function (t, e, n, a, i, o, r, l, c) {
|
||||
t.save(),
|
||||
t.translate(e, n),
|
||||
t.rotate(o),
|
||||
t.scale(a, i),
|
||||
t.arc(0, 0, 1, r, l, c),
|
||||
t.restore()
|
||||
})(
|
||||
t,
|
||||
e.x,
|
||||
e.y,
|
||||
Math.abs(o - a) * e.ovalScalar,
|
||||
Math.abs(r - i) * e.ovalScalar,
|
||||
(Math.PI / 10) * e.wobble,
|
||||
0,
|
||||
2 * Math.PI
|
||||
)
|
||||
: (t.moveTo(Math.floor(e.x), Math.floor(e.y)),
|
||||
t.lineTo(Math.floor(e.wobbleX), Math.floor(i)),
|
||||
t.lineTo(Math.floor(o), Math.floor(r)),
|
||||
t.lineTo(Math.floor(a), Math.floor(e.wobbleY))),
|
||||
t.closePath(),
|
||||
t.fill(),
|
||||
e.tick < e.totalTicks
|
||||
)
|
||||
})(d, t)
|
||||
})).length
|
||||
? (c = b.frame(e))
|
||||
: l()
|
||||
})),
|
||||
(s = l)
|
||||
})
|
||||
return {
|
||||
addFettis: function (t) {
|
||||
return (u = u.concat(t)), f
|
||||
},
|
||||
canvas: t,
|
||||
promise: f,
|
||||
reset: function () {
|
||||
c && b.cancel(c), s && s()
|
||||
}
|
||||
}
|
||||
}
|
||||
function E(t, n) {
|
||||
var a,
|
||||
i = !t,
|
||||
r = !!p(n || {}, 'resize'),
|
||||
c = p(n, 'disableForReducedMotion', Boolean),
|
||||
s = o && !!p(n || {}, 'useWorker') ? v() : null,
|
||||
u = i ? k : I,
|
||||
d = !(!t || !s) && !!t.__confetti_initialized,
|
||||
f =
|
||||
'function' == typeof matchMedia &&
|
||||
matchMedia('(prefers-reduced-motion)').matches
|
||||
function h(e, n, i) {
|
||||
for (
|
||||
var o,
|
||||
r,
|
||||
l,
|
||||
c,
|
||||
s,
|
||||
d = p(e, 'particleCount', M),
|
||||
f = p(e, 'angle', Number),
|
||||
h = p(e, 'spread', Number),
|
||||
g = p(e, 'startVelocity', Number),
|
||||
m = p(e, 'decay', Number),
|
||||
b = p(e, 'gravity', Number),
|
||||
v = p(e, 'drift', Number),
|
||||
y = p(e, 'colors', x),
|
||||
w = p(e, 'ticks', Number),
|
||||
C = p(e, 'shapes'),
|
||||
k = p(e, 'scalar'),
|
||||
I = (function (t) {
|
||||
var e = p(t, 'origin', Object)
|
||||
return (e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e
|
||||
})(e),
|
||||
E = d,
|
||||
S = [],
|
||||
F = t.width * I.x,
|
||||
N = t.height * I.y;
|
||||
E--;
|
||||
|
||||
)
|
||||
S.push(
|
||||
((o = {
|
||||
x: F,
|
||||
y: N,
|
||||
angle: f,
|
||||
spread: h,
|
||||
startVelocity: g,
|
||||
color: y[E % y.length],
|
||||
shape:
|
||||
C[
|
||||
((c = 0),
|
||||
(s = C.length),
|
||||
Math.floor(Math.random() * (s - c)) + c)
|
||||
],
|
||||
ticks: w,
|
||||
decay: m,
|
||||
gravity: b,
|
||||
drift: v,
|
||||
scalar: k
|
||||
}),
|
||||
(r = void 0),
|
||||
(l = void 0),
|
||||
(r = o.angle * (Math.PI / 180)),
|
||||
(l = o.spread * (Math.PI / 180)),
|
||||
{
|
||||
x: o.x,
|
||||
y: o.y,
|
||||
wobble: 10 * Math.random(),
|
||||
velocity: 0.5 * o.startVelocity + Math.random() * o.startVelocity,
|
||||
angle2D: -r + (0.5 * l - Math.random() * l),
|
||||
tiltAngle: Math.random() * Math.PI,
|
||||
color: o.color,
|
||||
shape: o.shape,
|
||||
tick: 0,
|
||||
totalTicks: o.ticks,
|
||||
decay: o.decay,
|
||||
drift: o.drift,
|
||||
random: Math.random() + 5,
|
||||
tiltSin: 0,
|
||||
tiltCos: 0,
|
||||
wobbleX: 0,
|
||||
wobbleY: 0,
|
||||
gravity: 3 * o.gravity,
|
||||
ovalScalar: 0.6,
|
||||
scalar: o.scalar
|
||||
})
|
||||
)
|
||||
return a ? a.addFettis(S) : (a = T(t, S, u, n, i)).promise
|
||||
}
|
||||
function g(n) {
|
||||
var o = c || p(n, 'disableForReducedMotion', Boolean),
|
||||
g = p(n, 'zIndex', Number)
|
||||
if (o && f)
|
||||
return l(function (t) {
|
||||
t()
|
||||
})
|
||||
i && a
|
||||
? (t = a.canvas)
|
||||
: i &&
|
||||
!t &&
|
||||
((t = (function (t) {
|
||||
var e = document.createElement('canvas')
|
||||
return (
|
||||
(e.style.position = 'fixed'),
|
||||
(e.style.top = '0px'),
|
||||
(e.style.left = '0px'),
|
||||
(e.style.pointerEvents = 'none'),
|
||||
(e.style.zIndex = t),
|
||||
e
|
||||
)
|
||||
})(g)),
|
||||
document.body.appendChild(t)),
|
||||
r && !d && u(t)
|
||||
var m = {width: t.width, height: t.height}
|
||||
function b() {
|
||||
if (s) {
|
||||
var e = {
|
||||
getBoundingClientRect: function () {
|
||||
if (!i) return t.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
return (
|
||||
u(e),
|
||||
void s.postMessage({resize: {width: e.width, height: e.height}})
|
||||
)
|
||||
}
|
||||
m.width = m.height = null
|
||||
}
|
||||
function v() {
|
||||
;(a = null),
|
||||
r && e.removeEventListener('resize', b),
|
||||
i && t && (document.body.removeChild(t), (t = null), (d = !1))
|
||||
}
|
||||
return (
|
||||
s && !d && s.init(t),
|
||||
(d = !0),
|
||||
s && (t.__confetti_initialized = !0),
|
||||
r && e.addEventListener('resize', b, !1),
|
||||
s ? s.fire(n, m, v) : h(n, m, v)
|
||||
)
|
||||
}
|
||||
return (
|
||||
(g.reset = function () {
|
||||
s && s.reset(), a && a.reset()
|
||||
}),
|
||||
g
|
||||
)
|
||||
}
|
||||
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E)
|
||||
})(
|
||||
(function () {
|
||||
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
|
||||
})(),
|
||||
e,
|
||||
!1
|
||||
),
|
||||
(t.confetti = e.exports)
|
||||
})(window, {})
|
||||
@@ -1,6 +1,6 @@
|
||||
// update cache version every time there is a new deployment
|
||||
// so the service worker reinitializes the cache
|
||||
const CACHE_VERSION = 107
|
||||
const CACHE_VERSION = 114
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
|
||||
@@ -860,11 +860,11 @@ new Vue({
|
||||
this.disclaimerDialog.show = true
|
||||
this.$q.localStorage.set('lnbits.disclaimerShown', true)
|
||||
}
|
||||
|
||||
// listen to incoming payments
|
||||
LNbits.events.onInvoicePaid(this.g.wallet, payment =>
|
||||
this.onPaymentReceived(payment.payment_hash)
|
||||
)
|
||||
eventReactionWebocket(wallet.id)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
"css": ["vendor/quasar.css", "vendor/Chart.css", "css/base.css"]
|
||||
|
||||
@@ -14,11 +14,9 @@ from pywebpush import WebPushException, webpush
|
||||
from lnbits.core.crud import (
|
||||
delete_expired_invoices,
|
||||
delete_webpush_subscriptions,
|
||||
get_balance_checks,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
)
|
||||
from lnbits.core.services import redeem_lnurl_withdraw
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_wallet_class
|
||||
|
||||
@@ -186,14 +184,6 @@ async def check_pending_payments():
|
||||
await asyncio.sleep(60 * 30) # every 30 minutes
|
||||
|
||||
|
||||
async def perform_balance_checks():
|
||||
while True:
|
||||
for bc in await get_balance_checks():
|
||||
await redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||
|
||||
await asyncio.sleep(60 * 60 * 6) # every 6 hours
|
||||
|
||||
|
||||
async def invoice_callback_dispatcher(checking_id: str):
|
||||
"""
|
||||
Takes incoming payments, sets pending=False, and dispatches them to
|
||||
|
||||
Generated
+345
-755
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@
|
||||
"js/components.js",
|
||||
"js/components/lnbits-funding-sources.js",
|
||||
"js/components/extension-settings.js",
|
||||
"js/event-reactions.js",
|
||||
"js/bolt11-decoder.js"
|
||||
],
|
||||
"css": [
|
||||
|
||||
Generated
+1
-1
@@ -2862,4 +2862,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "c29cac992fae552eb8da791e13fe7766eab7571e260a42cc0de0d755508c42b7"
|
||||
content-hash = "b2e21e0075047150888581a401d46fbe8efd6226e85a85189c3f3db51f825a48"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "lnbits"
|
||||
version = "0.12.0"
|
||||
version = "0.12.1"
|
||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ is_regtest: bool = not is_fake
|
||||
docker_lightning_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-lnd-1-1",
|
||||
"lnbits-lnd-1-1",
|
||||
"lncli",
|
||||
"--network",
|
||||
"regtest",
|
||||
@@ -46,7 +46,7 @@ docker_lightning_cli = [
|
||||
docker_bitcoin_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-bitcoind-1-1" "bitcoin-cli",
|
||||
"lnbits-bitcoind-1-1" "bitcoin-cli",
|
||||
"-rpcuser=lnbits",
|
||||
"-rpcpassword=lnbits",
|
||||
"-regtest",
|
||||
@@ -56,7 +56,7 @@ docker_bitcoin_cli = [
|
||||
docker_lightning_unconnected_cli = [
|
||||
"docker",
|
||||
"exec",
|
||||
"lnbits-legend-lnd-2-1",
|
||||
"lnbits-lnd-2-1",
|
||||
"lncli",
|
||||
"--network",
|
||||
"regtest",
|
||||
|
||||
Reference in New Issue
Block a user