Compare commits

...
16 Commits
Author SHA1 Message Date
dni ⚡andGitHub 11173e6460 fix: github release missing checkout (#2264)
actions/checkout was missing so it could not release
2024-02-12 13:00:03 +01:00
dni ⚡andGitHub 069a7922fc chore: update to 0.12.1 (#2262) 2024-02-12 12:43:20 +01:00
98338ffb00 Improves payment reactions (#2251)
* updated to select

* updating

* working

* bundle

* updated to select

* updating

* working

* bundle

* Make room for everything apart from sending

* Update lnbits/static/js/base.js

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>

* prettier

* bundled

* fundle

* changed to window[]

* vlads suggestion

* added stars

* vlads suggestion

* fundle

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-12 10:48:07 +00:00
Pavol Rusnak 62dec118af chore: make bundle 2024-02-09 12:36:07 -03:00
tlindiandPavol Rusnak 84b406534d Update fi.js 2024-02-09 12:36:07 -03:00
d9d2d59b73 Wallet limits: max balance, daily max withdraw, transactions per sec (#2223)
Co-authored-by: benarc <ben@arc.wales>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-09 17:25:53 +02:00
20d4b954c0 fix: custom site desc (#2254)
* custom site desc

* fiddled formatting

---------

Co-authored-by: arcbtc <ben@arc.wales>
2024-02-09 15:08:02 +00:00
dni ⚡andGitHub 5edb845d8f feat: add setting lnbits_allow_new_accounts to admin ui (#2252)
added to users tab of admin ui
2024-02-09 14:54:30 +02:00
cfe2b43d2a Improved env layout (#2226)
* Improved env layout

* Update .env.example

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-09 14:34:10 +02:00
Vlad StanandGitHub a0ac41a9ce fix: use get_dbversions (#2255) 2024-02-09 14:10:51 +02:00
dni ⚡andGitHub 194a527736 fix: regtest use new docker hub address (#2248)
* fix: regtest use new docker hub address
2024-02-09 08:46:33 +01:00
dni ⚡andGitHub 6f135c0696 feat: add monitor admin endpoint for listeners (#2242)
this can be useful when debugging invoice listeners. in the future there coud be some monitor dashboard on the admin ui.
2024-02-09 08:08:37 +01:00
dni ⚡andGitHub 9a767b627f chore: dead code perform_balance_check (#2244)
this code is not used anywhere on core nor on extensions
2024-02-08 15:01:23 +01:00
c1c4eda69d Adds optional confetti to incoming payments in wallet (#2231)
* initial

* Working with toggle

* Prettier and bundle

* removed console.log

* bundle

* fix: i18n

* refactor: simplify logic

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-08 13:34:03 +00:00
dni ⚡andGitHub 6c46867698 chore: update readme to use new docker alias (#2246)
`docker pull lnbits/lnbits`
2024-02-08 13:24:55 +00:00
dni ⚡andGitHub a73eb569ae bug: docker upload workflow (#2245)
something changed in the docker-build-push action.

added `workflow_dispatch` to be able to manually run those docker releases via github
2024-02-08 11:30:38 +01:00
28 changed files with 1185 additions and 852 deletions
+30 -21
View File
@@ -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"
+7 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+1
View File
@@ -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 }}
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 />
+20 -6
View File
@@ -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>
+17
View File
@@ -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>
+30 -26
View File
@@ -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>
+15
View File
@@ -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),
+10
View File
@@ -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")
+9 -9
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -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',
+4 -3
View File
@@ -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',
+9
View File
@@ -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)
+4 -1
View File
@@ -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')
+560
View File
@@ -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 -1
View File
@@ -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 => {
+1 -1
View File
@@ -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)
}
})
+1
View File
@@ -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"]
-10
View File
@@ -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
+345 -755
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",