Compare commits

...
58 Commits
Author SHA1 Message Date
ArcandGitHub 921984e52b mre formats (#1733) 2023-05-24 14:27:16 +01:00
ArcandGitHub d0599458c6 Format wallet UI tweaks (#1732)
* Formatting wallet stuff

* Bundle fundle

* Added v-if
2023-05-24 13:41:03 +01:00
Vlad StanandGitHub 80b803913f feat: print extensions version on install/uninstall/reinstall (#1726) 2023-05-24 11:58:11 +01:00
Tiago VasconcelosandGitHub 6eab3dae01 add loop for language selection (#1725)
add languages

add portuguese

fix bug with singlequotes, and a vendor.json

fixup formatting

include bundle

rename files
2023-05-24 11:43:31 +01:00
Tiago VasconcelosandGitHub eab19280d5 fix "boolean" query for sqlite (#1727)
use false
2023-05-24 12:14:21 +02:00
ArcandGitHub c960f718f5 Adds websocket for incoming/outgoing payments with wallet balance (#1679)
* Websocket works for incoming/outgoing and includes wallet balance

* mypy

* mypy try

* Actual mypy (no try)

* format
2023-05-22 12:38:26 +01:00
7df131d3c7 Minor improvements to the German translation (#1721)
* Minor improvements to the German translation

This replaces some usages of "Sie" with "du", and also fixes a few minor mistakes.

* format

---------

Co-authored-by: ben <ben@arc.wales>
2023-05-22 12:22:51 +01:00
dni ⚡andGitHub cae24b233e fix formatting on main (#1723) 2023-05-22 12:21:11 +01:00
jackstar12andGitHub 252fd6c313 feat: specify sort fields (#1716)
* feat: specify sort fields

* doc string
2023-05-22 12:11:05 +01:00
37ac630573 add voltage logo to index.html under runs on section (#1711)
* add voltage to index.html under runs on section

* fixed url

---------

Co-authored-by: Jose Sahagun <jsahagun91@gmail.com>
2023-05-22 12:10:21 +01:00
dni ⚡andGitHub 035d6263f9 Bump version to 0.10.8 (#1717) 2023-05-22 12:09:45 +01:00
dni ⚡andGitHub 0ab1924327 remove mypy from precommit (#1718) 2023-05-22 12:09:21 +01:00
Aaron DewesandGitHub 127b780af7 C-Lightning -> Core Lightning (#1720)
They rebranded a while ago, so this changes the GUI texts to reflect that
2023-05-22 12:06:23 +01:00
daywalker90andGitHub 39066e4bc8 fix cln wallet for msat migration (#1671)
* fix cln wallet for msat migration

* status wants our funds only
2023-05-22 12:03:37 +01:00
ArcandGitHub ca41a92bd9 version update (#1715) 2023-05-19 19:09:17 +01:00
dni ⚡andGitHub f97c698917 FIX: qrcode scanner on legends, by updating the library (#1714) 2023-05-19 13:23:22 +01:00
dni ⚡andGitHub dd1c17f86f FIX: internal qrcode scanning check should always check for lowercase (#1713)
fixup
2023-05-19 12:41:58 +01:00
Tiago VasconcelosandGitHub 587b763f03 Add user role (#1712)
* Add user role

* make format
2023-05-19 09:32:02 +02:00
callebtcandGitHub e39241a9e9 Fix: get_payments crud default Filter values. (#1703)
* get_payments: deafult values for filter

* lololololoolol
2023-05-11 18:36:20 +02:00
callebtc b6223f0e6d get_payments: deafult values for filter 2023-05-11 18:29:08 +02:00
6f2771e334 Merges extensions into one page (#1656)
* Merged extensions into one page

* Bundle files updated

* Fixed install bug

* feat: client side version compatibility check

* fix: hide `Activated/Deactivated` toggle for non-admins

* feat: translate labels to `EN`

* feat: add other language translations

* chore: update bundle for i18n

* feat: check extension version server-side

* feat: show warning message

* refactor: nicer mapping

Co-authored-by: dni  <office@dnilabs.com>

* chore: code format

* chore: extra log

* feat: check_latest_version of ext

* feat: show tooltip for new version

* chore: `make bundle`

* chore: `mypy`

* chore: code clean-up

* feat: show version in badge (spacing is fine)

* chore: make bundle

* feat: check `min_lnbits_version` and `warning` in `config.json`

* chore: code formatting

* chore: downgrade log level

* fix: extract `ExtensionsInstallSettings` as readonly

* fix: do not show installed and deactivated extensions

* chore: format

* fix: `Enable` button tooltip

* fix: set installed release after installation

* fix: hide deactivated extensions from regular users

* bundle fundle

* bundle fundle

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: dni  <office@dnilabs.com>
2023-05-11 01:14:07 +01:00
3fe33dfb81 Translated en.js to Polish (#1701)
* Translated en.js to Polish

* Minor pl.js fixes

* Added to language menu

* Update bundle

---------

Co-authored-by: ben <ben@arc.wales>
2023-05-11 01:01:35 +01:00
4ec16b56bf Extend bar for mobile (#1544)
* extend bar for mobile

* smaller padding

* center buttons

* Added rounded style

* Added icons, looks better

* service worker

* Changed icon

* Fixed show if

---------

Co-authored-by: ben <ben@arc.wales>
2023-05-11 00:12:48 +01:00
ArcandGitHub 662f792d4f Added Welsh (#1696)
* Added Welsh

* format

* Added to base

* bundle fundle
2023-05-10 11:18:18 +01:00
dni ⚡andGitHub d8ccb4dcd8 fix old payments endpoint for lndhub (#1698)
* fix old payments endpoint for lndhub

* fixup tests
2023-05-10 11:47:04 +02:00
7e0beb508c Adding IT properly (#1695)
* Adding IT properly

* added to base + format + bundle

---------

Co-authored-by: ben <ben@arc.wales>
2023-05-10 10:33:17 +01:00
dni ⚡andGitHub 95071eb87e quick fix languages bundle (#1691) 2023-05-09 09:56:44 +01:00
BitkarrotandGitHub 44c5c87405 Add troubleshooting note for Poetry errors on OSX install (#1641)
* Update installation.md

for poetry errors on OSX

* Update installation.md

* Update installation.md

* Update installation.md

remove trouble shooting message at top
2023-05-09 09:43:19 +01:00
dni ⚡andGitHub 8e8093bef3 de.js i18n updates, add nl and pirate (#1675)
bump service worker
2023-05-09 09:42:27 +01:00
dni ⚡andGitHub bb9ef7f88c FEAT: precommit git hooks, based on fusions #913 (#1645)
* init commit for precommit

* some reformatting precommit does
2023-05-09 09:41:40 +01:00
Vlad StanandGitHub 67d8b67ee5 Create super user account if it does not exist (#1688)
* fix: temp create account for `super_user_id` if missing

* chore: remove dumb import

* refactor: move logic outside `crud`

* feat: add uuid4 conversion

* fix: require valid string in .env file

* fix: update the `settings.super_user` value in case or normalisation for UUID4

* fix: allow long super_user

* chore: code format

* fix: add UI redirect with the normalized user

* fix: normalize `super_user` up one level

* fix: should normalize user in non-ui mode also
2023-05-09 09:22:19 +01:00
Vlad StanandGitHub 132d8a9320 fix: rename bool super_user to is_super_user (#1690) 2023-05-09 09:20:51 +01:00
c0f66989cb Serverside Pagination for payments (#1613)
* initial backend support

* implement payments pagination on frontend

* implement search for payments api

* fix pyright issues

* sqlite support for searching

* backwards compatability

* formatting, small fixes

* small optimization

* fix sorting issue, add error handling

* GET payments test

* filter by dates, use List instead of list

* fix sqlite

* update bundle

* test old payments endpoint aswell

* refactor for easier review

* optimise test

* revert unnecessary change

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-05-09 09:18:53 +01:00
dni ⚡andGitHub 45b199a8ef create datadir on launch (#1674)
* create datadir on launch
* change makedirs to Path.mkdir
2023-05-09 10:04:27 +02:00
dni ⚡andGitHub 159aa24a4e update 0.10.6 (#1689) 2023-05-09 09:48:05 +02:00
62812938bd French language added (#1686)
* French language added
---------
Co-authored-by: Dood597 <66718486+Dood597@users.noreply.github.com>
2023-05-09 09:47:06 +02:00
dni ⚡andGitHub 3c92ef2900 FIX: #1661 (#1663)
fixup
2023-05-04 17:22:24 +02:00
dni ⚡andGitHub 7feac3bb5e add lnbits version to frontend (#1677) 2023-05-04 17:21:37 +02:00
Tiago VasconcelosandGitHub 590b52cb31 Update print.html (#1678)
* Update print.html

Fix print.html not getting the JS libraries

* Update print.html

Add `INCLUDED_CSS` also
2023-05-04 17:21:18 +02:00
callebtcandGitHub b4fce8b00b fix internal invoice check (#1681)
* fix internal invoice check

* return error

* make format
2023-05-04 17:20:37 +02:00
ArcandGitHub bd47527d34 Merge pull request #1672 from lnbits/paymentsocketjson
Output json to payment websocket
2023-05-02 18:42:51 +01:00
ben 171aa08e0c output json 2023-05-02 18:40:46 +01:00
ArcandGitHub bc2c5fd1a9 Merge pull request #1648 from lnbits/typeignore-generic
removing type ignores from views/generic.py
2023-05-02 14:50:00 +01:00
ArcandGitHub 67c377affc Merge pull request #1670 from lnbits/fixsparkimage
Fixes spark image on homepage
2023-05-02 14:25:59 +01:00
ben 09d67249cc format 2023-05-02 14:11:01 +01:00
ben 068965d81b Missing div 2023-05-02 14:07:25 +01:00
ben aec15ae820 format 2023-05-02 13:43:54 +01:00
ben c714a2641d Adds websocket for payments 2023-05-02 12:53:36 +01:00
ben b9d3fc5744 Fix spark image 2023-05-02 12:27:51 +01:00
dni ⚡andcalle 21157fffe2 removing type ignores from views/generic.py
suggestion motorina1

Update lnbits/core/views/generic.py

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>

quickfix
2023-04-27 16:28:16 +02:00
dni ⚡andGitHub f8058825d8 FEAT: introduce bundle_assets variable and decouple it from DEBUG (#1660) 2023-04-27 16:05:49 +02:00
dni ⚡andGitHub bfb27020ee regression: fatfinger removed block styles (#1659)
quickfix

format
2023-04-27 15:38:52 +02:00
dni ⚡andGitHub 667de4d1f0 generate .super_user file on startup if adminui is enabled (#1651)
* generate .super_user file on startup if adminui is enabled
* only save super_user id instead of url
2023-04-25 10:25:50 +02:00
dni ⚡andGitHub 5c119e903d increment cache version for service-worker.js (#1644)
* increment cache version
* add serviceworker incrementer on make bundle
2023-04-25 10:25:09 +02:00
dni ⚡andGitHub 3f3af53240 prepare lnbits version 0.10.5 (#1649) 2023-04-25 10:24:24 +02:00
dni ⚡andGitHub 1b84ebf13d FEAT: build static files with npm (sass, concat, minify), remove build step from python, include minified bundle files (#1601) 2023-04-21 14:51:46 +02:00
dni ⚡andGitHub bef2d4a72e remove boltz_client as dependecy (#1610) 2023-04-21 14:42:52 +02:00
dni ⚡andGitHub 04471b8b5b remove python38 from pyproject.toml (#1632) 2023-04-21 14:37:26 +02:00
67 changed files with 15610 additions and 1245 deletions
+5 -4
View File
@@ -1,10 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
title: '[BUG]'
labels: bug
assignees: ''
---
**Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- LNbits version: [e.g. 0.9.2 or commit hash]
- Database [e.g. sqlite, postgres]
- LNbits version: [e.g. 0.9.2 or commit hash]
- Database [e.g. sqlite, postgres]
**Additional context**
Add any other context about the problem here.
+1 -2
View File
@@ -1,10 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature request]"
title: '[Feature request]'
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
-3
View File
@@ -4,7 +4,4 @@ about: Anything else that you need to say
title: ''
labels: ''
assignees: ''
---
+3 -1
View File
@@ -23,6 +23,7 @@ tests/data/*.sqlite3
*.pyc
*.env
.env
.super_user
data
*.sqlite3
@@ -32,7 +33,8 @@ __bundle__
coverage.xml
node_modules
lnbits/static/bundle.*
lnbits/static/bundle.js
lnbits/static/bundle.css
docker
# Nix
+34
View File
@@ -0,0 +1,34 @@
exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/extensions/.*|^lnbits/wallets/lnd_grpc_files/.*|^package-lock.json$'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-docstring-first
- id: check-json
- id: debug-statements
- id: mixed-line-ending
- id: check-case-conflict
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: ['--profile', 'black']
- repo: https://github.com/pre-commit/mirrors-prettier
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
hooks:
- id: prettier
args: ['lnbits']
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
entry: poetry run flake8
+23 -16
View File
@@ -73,20 +73,27 @@ bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
#
updatevendor:
sass:
npm run sass
bundle:
npm install
cp ./node_modules/moment/moment.js ./lnbits/static/vendor/
cp ./node_modules/underscore/underscore.js ./lnbits/static/vendor/
cp ./node_modules/axios/dist/axios.js ./lnbits/static/vendor/
cp ./node_modules/vue/dist/vue.js ./lnbits/static/vendor/
cp ./node_modules/vue-router/dist/vue-router.js ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.browser.js ./lnbits/static/vendor/
cp ./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js ./lnbits/static/vendor/
cp ./node_modules/vuex/dist/vuex.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.umd.js ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.bundle.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.css ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.css ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.css ./lnbits/static/vendor/
cp ./node_modules/vue-i18n/dist/vue-i18n.js ./lnbits/static/vendor/
npm run sass
npm run vendor_copy
npm run vendor_json
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
npm run vendor_bundle_css
npm run vendor_minify_css
npm run vendor_bundle_js
npm run vendor_minify_js
# increment serviceworker version
sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \
lnbits/core/static/js/service-worker.js
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
pre-commit:
poetry run pre-commit run --all-files
+7 -10
View File
@@ -1,10 +1,8 @@
LNbits
======
# LNbits
[![license-badge]](LICENSE)
[![docs-badge]][docs]
![Lightning network wallet](https://i.imgur.com/EHvK6Lq.png)
# LNbits v0.10 BETA, free and open-source Lightning wallet accounts system
@@ -17,11 +15,11 @@ Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits serve
LNbits is a Python server that sits on top of any funding source. It can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
* Extendable platform for exploring Lightning network functionality via the LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
- Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
- Extendable platform for exploring Lightning network functionality via the LNbits extension framework
- Part of a development stack via LNbits API
- Fallback wallet for the LNURL scheme
- Instant wallet for LN demonstrations
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, LightningTipBot, and with more being added regularly.
@@ -60,7 +58,7 @@ Example use would be an ATM, which utilises LNURL, if the user scans the QR with
## LNbits as an instant wallet
Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app".
Wallets can be easily generated and given out to people at events. "Go to this website", has a lot less friction than "Download this app".
![lnurl ATM](https://i.imgur.com/xFWDnwy.png)
@@ -68,7 +66,6 @@ Wallets can be easily generated and given out to people at events. "Go to this
If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://github.com/lnbits/lnbits/wiki
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
+1 -5
View File
@@ -12,7 +12,7 @@ By default, LNbits will use SQLite as its database. You can also use PostgreSQL
## Option 1 (recommended): poetry
If you have problems installing LNbits using these instructions, please have a look at the [Troubleshooting](#troubleshooting) section.
Mininum poetry version has is ^1.2, but it is recommended to use latest poetry. (including OSX)
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -190,10 +190,6 @@ sudo apt install python3.9-dev gcc build-essential
poetry add setuptools wheel
```
#### Poetry
If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
+1 -1
View File
@@ -18,7 +18,7 @@ A backend wallet can be configured using the following LNbits environment variab
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
### Spark (c-lightning)
### Spark (Core Lightning)
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
- `SPARK_URL`: http://10.147.17.230:9737/rpc
+6 -2
View File
@@ -157,10 +157,14 @@ async def check_installed_extensions(app: FastAPI):
installed = check_installed_extension(ext)
if not installed:
await restore_installed_extension(app, ext)
logger.info(f"✔️ Successfully re-installed extension: {ext.id}")
logger.info(
f"✔️ Successfully re-installed extension: {ext.id} ({ext.installed_version})"
)
except Exception as e:
logger.warning(e)
logger.warning(f"Failed to re-install extension: {ext.id}")
logger.warning(
f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
)
async def build_all_installed_extensions_list() -> List[InstallableExtension]:
-19
View File
@@ -1,6 +1,4 @@
import asyncio
import os
import warnings
import click
from loguru import logger
@@ -20,23 +18,6 @@ def db_migrate():
asyncio.create_task(migrate_databases())
@click.command("assets")
def handle_assets():
transpile_scss()
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(settings.lnbits_path, "static/scss/base.scss")) as scss:
with open(
os.path.join(settings.lnbits_path, "static/css/base.css"), "w"
) as css:
css.write(compile_string(scss.read()))
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
+66 -34
View File
@@ -2,24 +2,31 @@ import datetime
import json
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from uuid import uuid4
from uuid import UUID, uuid4
import shortuuid
from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection, Filters
from lnbits.db import Connection, Filters, Page
from lnbits.extension_manager import InstallableExtension
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
from . import db
from .models import BalanceCheck, Payment, TinyURL, User, Wallet
from .models import BalanceCheck, Payment, PaymentFilters, TinyURL, User, Wallet
# accounts
# --------
async def create_account(conn: Optional[Connection] = None) -> User:
user_id = uuid4().hex
async def create_account(
conn: Optional[Connection] = None, user_id: Optional[str] = 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
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = await get_account(user_id=user_id, conn=conn)
@@ -329,7 +336,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
rows = await db.fetchall(
f"""
SELECT * FROM apipayments
WHERE pending = 'false'
WHERE pending = false
AND extra LIKE ?
AND extra LIKE ?
ORDER BY time DESC LIMIT {limit}
@@ -343,7 +350,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
return rows
async def get_payments(
async def get_payments_paginated(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
@@ -352,28 +359,23 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[Payment]] = None,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
) -> Page[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
args: List[Any] = []
values: List[Any] = []
clause: List[str] = []
if since is not None:
if db.type == POSTGRES:
clause.append("time > to_timestamp(?)")
elif db.type == COCKROACH:
clause.append("time > cast(? AS timestamp)")
else:
clause.append("time > ?")
args.append(since)
clause.append(f"time > {db.timestamp_placeholder}")
values.append(since)
if wallet_id:
clause.append("wallet = ?")
args.append(wallet_id)
values.append(wallet_id)
if complete and pending:
pass
@@ -397,21 +399,53 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
if not filters:
filters = Filters(limit=None, offset=None)
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{filters.where(clause)}
ORDER BY time DESC
{filters.pagination()}
""",
filters.values(args),
return await (conn or db).fetch_page(
"SELECT * FROM apipayments",
clause,
values,
filters=filters,
model=Payment,
)
return [Payment.from_row(row) for row in rows]
async def get_payments(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> list[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
filters = filters or Filters()
filters.sortby = filters.sortby or "time"
filters.direction = filters.direction or "desc"
filters.limit = limit or filters.limit
filters.offset = offset or filters.offset
page = await get_payments_paginated(
wallet_id=wallet_id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
since=since,
exclude_uncheckable=exclude_uncheckable,
filters=filters,
conn=conn,
)
return page.data
async def delete_expired_invoices(
@@ -454,7 +488,6 @@ async def create_payment(
webhook: Optional[str] = None,
conn: Optional[Connection] = None,
) -> Payment:
# todo: add this when tests are fixed
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
# assert previous_payment is None, "Payment already exists"
@@ -514,7 +547,6 @@ async def update_payment_details(
new_checking_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> None:
set_clause: List[str] = []
set_variables: List[Any] = []
@@ -702,7 +734,7 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
row_dict = dict(sets)
row_dict.pop("super_user")
admin_settings = AdminSettings(
super_user=is_super_user,
is_super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
**row_dict,
)
+12
View File
@@ -1,6 +1,7 @@
import importlib
import re
from typing import Any
from uuid import UUID
import httpx
from loguru import logger
@@ -63,3 +64,14 @@ async def stop_extension_background_work(ext_id: str, user: str):
url = f"https://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
except Exception as ex:
logger.warning(ex)
def to_valid_user_id(user_id: str) -> UUID:
if len(user_id) < 32:
raise ValueError("User ID must have at least 128 bits")
try:
int(user_id, 16)
except:
raise ValueError("Invalid hex string for User ID.")
return UUID(hex=user_id[:32], version=4)
+20 -2
View File
@@ -11,7 +11,7 @@ from lnurl import encode as lnurl_encode
from loguru import logger
from pydantic import BaseModel
from lnbits.db import Connection
from lnbits.db import Connection, FilterModel, FromRowModel
from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class, settings
from lnbits.wallets.base import PaymentStatus
@@ -86,7 +86,7 @@ class User(BaseModel):
return False
class Payment(BaseModel):
class Payment(FromRowModel):
checking_id: str
pending: bool
amount: int
@@ -214,6 +214,24 @@ class Payment(BaseModel):
await delete_payment(self.checking_id, conn=conn)
class PaymentFilters(FilterModel):
__search_fields__ = ["memo", "amount"]
checking_id: str
amount: int
fee: int
memo: Optional[str]
time: datetime.datetime
bolt11: str
preimage: str
payment_hash: str
expiry: Optional[datetime.datetime]
extra: Dict = {}
wallet_id: str
webhook: Optional[str]
webhook_status: Optional[int]
class BalanceCheck(BaseModel):
wallet: str
service: str
+32 -4
View File
@@ -35,6 +35,7 @@ from .crud import (
create_wallet,
delete_wallet_payment,
get_account,
get_standalone_payment,
get_super_settings,
get_wallet,
get_wallet_payment,
@@ -42,6 +43,7 @@ from .crud import (
update_payment_status,
update_super_user,
)
from .helpers import to_valid_user_id
from .models import Payment
@@ -66,7 +68,6 @@ async def create_invoice(
internal: Optional[bool] = False,
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
if not amount > 0:
raise InvoiceFailure("Amountless invoices not supported.")
@@ -157,6 +158,18 @@ async def pay_invoice(
# check_internal() returns the checking_id of the invoice we're waiting for (pending only)
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id:
# perform additional checks on the internal payment
# the payment hash is not enough to make sure that this is the same invoice
internal_invoice = await get_standalone_payment(
internal_checking_id, incoming=True, conn=conn
)
assert internal_invoice is not None
if (
internal_invoice.amount != invoice.amount_msat
or internal_invoice.bolt11 != payment_request.lower()
):
raise PaymentFailure("Invalid invoice.")
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet
await create_payment(
@@ -234,6 +247,15 @@ async def pay_invoice(
new_checking_id=payment.checking_id,
conn=conn,
)
wallet = await get_wallet(wallet_id, conn=conn)
if wallet:
await websocketUpdater(
wallet_id,
{
"wallet_balance": wallet.balance or None,
"payment": payment._asdict(),
},
)
logger.debug(f"payment successful {payment.checking_id}")
elif payment.checking_id is None and payment.ok is False:
# payment failed
@@ -425,6 +447,9 @@ async def update_wallet_balance(wallet_id: str, amount: int):
async def check_admin_settings():
if settings.super_user:
settings.super_user = to_valid_user_id(settings.super_user).hex
if settings.lnbits_admin_ui:
settings_db = await get_super_settings()
if not settings_db:
@@ -447,6 +472,10 @@ async def check_admin_settings():
admin_url = f"{settings.lnbits_baseurl}wallet?usr={settings.super_user}"
logger.success(f"✔️ Access super user account at: {admin_url}")
# saving it to .super_user file
with open(".super_user", "w") as file:
file.write(settings.super_user)
# callback for saas
if (
settings.lnbits_saas_callback
@@ -462,7 +491,7 @@ def update_cached_settings(sets_dict: dict):
try:
setattr(settings, key, value)
except:
logger.error(f"error overriding setting: {key}, value: {value}")
logger.warning(f"Failed overriding setting: {key}, value: {value}")
if "super_user" in sets_dict:
setattr(settings, "super_user", sets_dict["super_user"])
@@ -472,8 +501,7 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
if super_user:
account = await get_account(super_user)
if not account:
account = await create_account()
super_user = account.id
account = await create_account(user_id=super_user)
if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id)
+3 -2
View File
@@ -1,5 +1,6 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
// update cache version every time there is a new deployment
// so the service worker reinitializes the cache
const CACHE_VERSION = 30
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {
+43 -24
View File
@@ -146,36 +146,36 @@ new Vue({
paymentsTable: {
columns: [
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'date',
name: 'time',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'date',
sortable: true
},
{
name: 'amount',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
}
],
pagination: {
rowsPerPage: 10
rowsPerPage: 10,
page: 1,
sortBy: 'time',
descending: true,
rowsNumber: 10
},
filter: null
filter: null,
loading: false
},
paymentsChart: {
show: false
@@ -695,16 +695,35 @@ new Vue({
LNbits.href.deleteWallet(walletId, user)
})
},
fetchPayments: function () {
return LNbits.api.getPayments(this.g.wallet).then(response => {
this.payments = response.data
.map(obj => {
fetchPayments: function (props) {
// Props are passed by qasar when pagination or sorting changes
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
this.paymentsTable.loading = true
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage,
sortby: pagination.sortBy ?? 'time',
direction: pagination.descending ? 'desc' : 'asc'
}
if (this.paymentsTable.filter) {
query.search = this.paymentsTable.filter
}
return LNbits.api
.getPayments(this.g.wallet, query)
.then(response => {
this.paymentsTable.loading = false
this.paymentsTable.pagination.rowsNumber = response.data.total
this.payments = response.data.data.map(obj => {
return LNbits.map.payment(obj)
})
.sort((a, b) => {
return b.time - a.time
})
})
})
.catch(err => {
this.paymentsTable.loading = false
LNbits.utils.notifyApiError(err)
})
},
fetchBalance: function () {
LNbits.api.getWallet(this.g.wallet).then(response => {
+11 -2
View File
@@ -7,8 +7,9 @@ from loguru import logger
from lnbits.tasks import SseListenersDict, register_invoice_listener
from . import db
from .crud import get_balance_notify
from .crud import get_balance_notify, get_wallet
from .models import Payment
from .services import websocketUpdater
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
"api_invoice_listeners"
@@ -37,7 +38,15 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
logger.trace("received invoice paid event")
# send information to sse channel
await dispatch_api_invoice_listeners(payment)
wallet = await get_wallet(payment.wallet_id)
if wallet:
await websocketUpdater(
payment.wallet_id,
{
"wallet_balance": wallet.balance or None,
"payment": payment._asdict(),
},
)
# dispatch webhook
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
+2 -2
View File
@@ -463,7 +463,7 @@
this.g.user.wallets[0].adminkey
)
.then(response => {
this.isSuperUser = response.data.super_user || false
this.isSuperUser = response.data.is_super_user || false
this.settings = response.data
this.formData = _.clone(this.settings)
this.updateFundingData()
@@ -474,7 +474,7 @@
},
updateSettings() {
let data = _.omit(this.formData, [
'super_user',
'is_super_user',
'lnbits_allowed_funding_sources'
])
LNbits.api
+541 -49
View File
@@ -1,46 +1,70 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script>
{% endblock %} {% block page %}
%} {{ window_vars(user, extensions) }}{% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-sm-9 gt-sm col-xs-12 mt-lg">
<p class="text-h4">
Extensions
<q-btn
flat
color="primary"
type="a"
:href="['/install?usr=', user.id].join('')"
:label="$t('manage_extensions')"
></q-btn>
</p>
<div class="col-sm-9 col-xs-12">
<p class="text-h4 gt-sm">{%raw%}{{ $t('extensions') }}{%endraw%}</p>
</div>
<div class="col-sm-3 col-xs-12 q-ml-auto">
<q-input v-model="searchTerm" label="Search extensions">
<q-input v-model="searchTerm" :label="$t('search_extensions')">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer q-mt-lg"
class="cursor-pointer"
/>
</q-input>
</div>
</div>
<p v-if="!filteredExtensions.length" v-text="$t('no_extensions')"></p>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-xs">
<div class="q-gutter-y-md">
<q-tabs
v-model="tab"
@input="handleTabChanged"
active-color="primary"
align="left"
>
<q-tab
name="installed"
:label="$t('installed')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="all"
:label="$t('all')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="featured"
:label="$t('featured')"
@update="val => tab = val.name"
></q-tab>
<i v-if="!g.user.admin && tab != 'installed'"
>{%raw%}{{ $t('only_admins_can_install') }}{%endraw%}</i
>
</q-tabs>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
v-for="extension in filteredExtensions"
:key="extension.code"
:key="extension.id + extension.hash"
>
<q-card>
<q-card-section style="min-height: 140px">
<q-card-section style="min-height: 140px" class="q-pb-none">
<div class="row">
<div class="col-3">
<q-img
v-if="extension.tile"
:src="extension.tile"
v-if="extension.icon"
:src="extension.icon"
spinner-color="white"
style="max-width: 100%"
></q-img>
@@ -60,6 +84,16 @@
</div>
</div>
<div class="col-9 q-pl-sm">
<q-badge
v-if="hasNewVersion(extension)"
color="green"
class="float-right"
>
<small>{%raw%}{{ $t('new_version') }}{%endraw%}</small>
<q-tooltip
><span v-text="extension.latestRelease.version"></span
></q-tooltip>
</q-badge>
{% raw %}
<div class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm">
{{ extension.name }}
@@ -88,6 +122,21 @@
{% endraw %}
</div>
</div>
<div class="row q-pt-sm">
<div class="col">
<small v-if="extension.dependencies?.length"
>{%raw%}{{ $t('extension_depends_on') }}{%endraw%}</small
>
<small v-else>&nbsp;</small>
<q-badge
v-for="dep in extension.dependencies"
:key="dep"
color="orange"
>
<small v-text="dep"></small>
</q-badge>
</div>
</div>
</q-card-section>
<q-card-section>
<div>
@@ -98,47 +147,490 @@
size="1.5em"
:max="5"
color="primary"
></q-rating>
><q-tooltip
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
></q-rating
>
<q-rating
v-model="maxStars"
class="lt-md"
size="1.5em"
:max="5"
color="primary"
></q-rating
><q-tooltip>Ratings coming soon</q-tooltip>
><q-tooltip
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
></q-rating
>
<q-toggle
v-if="extension.isAvailable && extension.isInstalled && g.user.admin"
:label="extension.isActive ? $t('activated'): $t('deactivated') "
color="secodary"
style="max-height: 21px"
v-model="extension.isActive"
@input="toggleExtension(extension)"
><q-tooltip
>{%raw%}{{ $t('activate_extension_details')
}}{%endraw%}</q-tooltip
></q-toggle
>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-actions>
<div v-if="extension.isEnabled">
<q-btn
flat
color="primary"
type="a"
:href="[extension.url, '?usr=', g.user.id].join('')"
>Open</q-btn
<q-card-actions style="min-height: 52px">
<div class="col-10">
<div v-if="!extension.inProgress">
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="primary"
type="a"
:href="[extension.id, '?usr=', g.user.id].join('')"
>{%raw%}{{ $t('open') }}{%endraw%}</q-btn
>
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="grey-5"
type="a"
:href="['{{
url_for('install.extensions')
}}', '?usr=', g.user.id, '&disable=', extension.id].join('')"
>
{%raw%}{{ $t('disable') }}{%endraw%}</q-btn
>
<q-btn
v-if="extension.isInstalled && !user.extensions.includes(extension.id) && extension.isActive"
flat
color="primary"
type="a"
:href="['{{
url_for('install.extensions')
}}', '?usr=', g.user.id, '&enable=', extension.id].join('')"
>
{%raw%}{{ $t('enable') }}{%endraw%}
<q-tooltip>
<span v-text="$t('enable_extension_details')">
</span> </q-tooltip
></q-btn>
<q-btn
@click="showUpgrade(extension)"
flat
color="primary"
v-if="g.user.admin"
>
{%raw%}{{ $t('manage') }}{%endraw%}<q-tooltip
>{%raw%}{{ $t('manage_extension_details')
}}{%endraw%}</q-tooltip
></q-btn
>
</div>
<div v-else>
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
</div>
<div class="col-2">
<div
v-if="extension.isInstalled && extension.installedRelease"
class="float-right"
>
<q-btn
flat
color="grey-5"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&disable=', extension.code].join('')"
>
Disable</q-btn
>
<q-badge>
{% raw %}{{ extension.installedRelease.version }}{% endraw
%}<q-tooltip
>{%raw%}{{ $t('extension_installed_version')
}}{%endraw%}</q-tooltip
>
</q-badge>
</div>
</div>
<q-btn
v-else
flat
color="primary"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')"
>
Enable</q-btn
>
</q-card-actions>
</q-card>
</div>
<q-dialog v-model="showUninstallDialog">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
<p>
{%raw%}{{ $t('extension_uninstall_warning') }}{%endraw%} <br />
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
</p>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="uninstallExtension()"
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showUpgradeDialog">
<q-card class="q-pa-lg lnbits__dialog-card">
<q-card-section>
<div class="text-h6" v-text="selectedExtension?.name"></div>
</q-card-section>
<div class="col-12 col-md-5 q-gutter-y-md" v-if="selectedExtensionRepos">
<q-card
flat
bordered
class="my-card"
v-for="repoName of Object.keys(selectedExtensionRepos)"
:key="repoName"
>
<q-expansion-item
:key="repoName"
group="repos"
:caption="repoName"
:content-inset-level="0.5"
:default-opened="selectedExtensionRepos[repoName].isInstalled"
>
<template v-slot:header>
<q-item-section avatar>
<q-avatar
:icon="selectedExtensionRepos[repoName].isInstalled ? 'download_done': 'download'"
:text-color="selectedExtensionRepos[repoName].isInstalled ? 'green' : ''"
/>
</q-item-section>
<q-item-section>
<div class="row">
<div class="col-10">
{%raw%}{{ $t('repository') }}{%endraw%}
<br />
<small v-text="repoName"></small>
</div>
<div class="col-2"></div>
</div>
</q-item-section>
</template>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
<q-expansion-item
v-for="release of selectedExtensionRepos[repoName].releases"
:key="release.version"
group="releases"
@click="getGitHubReleaseDetails(release)"
:icon="getReleaseIcon(release)"
:label="release.description"
:caption="release.version"
:content-inset-level="0.5"
:header-class="getReleaseIconColor(release)"
>
<div v-if="release.inProgress">
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
<div v-else-if="release.error">
<q-icon
class="gt-sm"
name="error"
color="pink"
size="70px"
></q-icon>
Cannot get the release details.
</div>
<q-card v-else>
<q-card-section v-if="release.is_version_compatible">
<q-btn
v-if="!release.isInstalled"
@click="installExtension(release)"
color="primary unelevated mt-lg pt-lg"
>{%raw%}{{ $t('install') }}{%endraw%}</q-btn
>
<q-btn v-else @click="showUninstall()" flat color="red">
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
>
<a
v-if="release.html_url"
class="text-secondary float-right"
:href="release.html_url"
target="_blank"
style="color: inherit"
>{%raw%}{{ $t('release_notes') }}{%endraw%}</a
>
</q-card-section>
<q-card-section v-else>
{%raw%}{{ $t('extension_min_lnbits_version') }}{%endraw%}
<strong>
<span v-text="release.min_lnbits_version"></span>
</strong>
</q-card-section>
<q-card v-if="release.warning">
<q-card-section>
<div class="text-h6">
<q-badge color="yellow" text-color="black">
{%raw%}{{ $t('warning') }}{%endraw%}
</q-badge>
</div>
<div class="text-subtitle2">
<span v-text="release.warning"></span>
</div>
</q-card-section>
</q-card>
<q-separator></q-separator> </q-card
></q-expansion-item>
</q-list>
</q-card-section>
</q-expansion-item>
</q-card>
</div>
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
<div class="row q-mt-lg">
<q-btn
v-if="selectedExtension?.isInstalled"
@click="showUninstall()"
flat
color="red"
>
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
tab: 'all',
filteredExtensions: null,
showUninstallDialog: false,
showUpgradeDialog: false,
selectedExtension: null,
selectedExtensionRepos: null,
maxStars: 5,
user: null
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e =>
tab === 'installed'
? e.isActive
? true
: !!this.g.user.admin
: true
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
this.tab = tab
},
installExtension: async function (release) {
const extension = this.selectedExtension
extension.inProgress = true
this.showUpgradeDialog = false
LNbits.api
.request(
'POST',
`/api/v1/extension?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo
}
)
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
extension.installedRelease = release
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showUpgradeDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
extension.installedRelease = null
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
toggleExtension: function (extension) {
const action = extension.isActive ? 'activate' : 'deactivate'
LNbits.api
.request(
'GET',
"{{ url_for('install.extensions') }}?usr=" +
this.g.user.id +
'&' +
action +
'=' +
extension.id
)
.then(response => {})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
showUninstall: function () {
this.showUpgradeDialog = false
this.showUninstallDialog = true
},
showUpgrade: async function (extension) {
this.selectedExtension = extension
this.showUpgradeDialog = true
this.selectedExtensionRepos = null
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extension.id}/releases?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
this.selectedExtensionRepos = data.reduce((repos, release) => {
repos[release.source_repo] = repos[release.source_repo] || {
releases: [],
isInstalled: false
}
release.inProgress = false
release.error = null
release.loaded = false
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
if (release.isInstalled) {
repos[release.source_repo].isInstalled = true
}
repos[release.source_repo].releases.push(release)
return repos
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
hasNewVersion: function (extension) {
if (extension.installedRelease && extension.latestRelease) {
return (
extension.installedRelease.version !==
extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
},
getReleaseIcon: function (release) {
if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done'
return 'download'
},
getReleaseIconColor: function (release) {
if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green'
return ''
},
getGitHubReleaseDetails: async function (release) {
if (!release.is_github_release || release.loaded) {
return
}
const [org, repo] = release.source_repo.split('/')
release.inProgress = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/release/${org}/${repo}/${release.version}?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
release.loaded = true
release.is_version_compatible = data.is_version_compatible
release.min_lnbits_version = data.min_lnbits_version
release.warning = data.warning
} catch (error) {
console.warn(error)
release.error = error
LNbits.utils.notifyApiError(error)
} finally {
release.inProgress = false
}
}
},
created: function () {
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
...e,
inProgress: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
if (window.user) {
this.user = LNbits.map.user(window.user)
}
},
mixins: [windowMixin]
})
</script>
{% endblock %}
+18 -8
View File
@@ -141,15 +141,25 @@
></q-img>
</a>
</div>
<div class="col q-pl-md">&nbsp;</div>
<div class="col q-pl-md">
<a href="https://github.com/shesek/spark-wallet">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/spark.png' : '/static/images/sparkl.png'"
></q-img>
</a>
</div>
</div>
<div class="col">
<a href="https://github.com/shesek/spark-wallet">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/spark.png' : '/static/images/sparkl.png'"
></q-img>
</a>
<div class="row">
<div class="col">
<a href="https://voltage.cloud">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/voltage.png' : '/static/images/voltagel.png'"
></q-img>
</a>
</div>
<div class="col q-pl-md"></div>
</div>
</div>
-505
View File
@@ -1,505 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {{ window_vars(user, extensions) }}{% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-sm-9 col-xs-12">
<p class="text-h4 gt-sm">Manage Extensions</p>
</div>
<div class="col-sm-3 col-xs-12 q-ml-auto">
<q-input v-model="searchTerm" label="Search extensions">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer"
/>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-xs">
<div class="q-gutter-y-md">
<q-tabs
v-model="tab"
@input="handleTabChanged"
active-color="primary"
align="left"
>
<q-tab
name="installed"
label="Installed"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="all"
label="All"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="featured"
label="Featured"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
v-for="extension in filteredExtensions"
:key="extension.id + extension.hash"
>
<q-card>
<q-card-section style="min-height: 140px">
<div class="row">
<div class="col-3">
<q-img
v-if="extension.icon"
:src="extension.icon"
spinner-color="white"
style="max-width: 100%"
></q-img>
<div v-else>
<q-icon
class="gt-sm"
name="extension"
color="primary"
size="70px"
></q-icon>
<q-icon
class="lt-md"
name="extension"
color="primary"
size="35px"
></q-icon>
</div>
</div>
<div class="col-9 q-pl-sm">
<q-badge
v-if="hasNewVersion(extension)"
color="green"
class="float-right"
>
<small>New Version</small>
</q-badge>
{% raw %}
<div class="text-h5 gt-sm q-mt-sm q-mb-xs gt-sm">
{{ extension.name }}
</div>
<div
class="text-h5 gt-sm q-mt-sm q-mb-xs lt-md"
style="min-height: 60px"
>
{{ extension.name }}
</div>
<div
class="text-subtitle2 gt-sm"
style="font-size: 11px; height: 34px"
>
{{ extension.shortDescription }}
</div>
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
{{ extension.name }}
</div>
<div
class="text-subtitle2 lt-md"
style="font-size: 9px; height: 34px"
>
{{ extension.shortDescription }}
</div>
{% endraw %}
</div>
</div>
<div class="row q-pt-sm">
<div class="col">
<small v-if="extension.dependencies?.length">Depends on:</small>
<small v-else>&nbsp;</small>
<q-badge
v-for="dep in extension.dependencies"
:key="dep"
color="orange"
>
<small v-text="dep"></small>
</q-badge>
</div>
</div>
</q-card-section>
<q-card-section>
<div>
<q-rating
class="gt-sm"
v-model="maxStars"
disable
size="1.5em"
:max="5"
color="primary"
></q-rating>
<q-rating
v-model="maxStars"
class="lt-md"
size="1.5em"
:max="5"
color="primary"
></q-rating
><q-tooltip>Ratings coming soon</q-tooltip>
</div>
</q-card-section>
<q-separator v-if="g.user.admin"></q-separator>
<q-card-actions>
<div class="col-10">
<div v-if="g.user.admin">
<div v-if="!extension.inProgress">
<q-btn @click="showUpgrade(extension)" flat color="primary">
Manage</q-btn
>
<q-toggle
v-if="extension.isAvailable && extension.isInstalled"
:label="extension.isActive ? 'Activated': 'Deactivated' "
color="secodary"
v-model="extension.isActive"
@input="toggleExtension(extension)"
></q-toggle>
</div>
<div v-else>
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
</div>
</div>
<div class="col-2">
<div class="float-right"></div>
</div>
</q-card-actions>
</q-card>
</div>
<q-dialog v-model="showUninstallDialog">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
<p>
You are about to remove the extension for all users. <br />
Are you sure you want to continue?
</p>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="uninstallExtension()"
>Yes, Uninstall</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showUpgradeDialog">
<q-card class="q-pa-lg lnbits__dialog-card">
<q-card-section>
<div class="text-h6" v-text="selectedExtension?.name"></div>
</q-card-section>
<div class="col-12 col-md-5 q-gutter-y-md" v-if="selectedExtensionRepos">
<q-card
flat
bordered
class="my-card"
v-for="repoName of Object.keys(selectedExtensionRepos)"
:key="repoName"
>
<q-expansion-item
:key="repoName"
group="repos"
:caption="repoName"
:content-inset-level="0.5"
:default-opened="selectedExtensionRepos[repoName].isInstalled"
>
<template v-slot:header>
<q-item-section avatar>
<q-avatar
:icon="selectedExtensionRepos[repoName].isInstalled ? 'download_done': 'download'"
:text-color="selectedExtensionRepos[repoName].isInstalled ? 'green' : ''"
/>
</q-item-section>
<q-item-section>
<div class="row">
<div class="col-10">
Repository
<br />
<small v-text="repoName"></small>
</div>
<div class="col-2">
<!-- <div v-if="selectedExtension.stars" class="float-right">
<small v-text="selectedExtension.stars"> </small>
<q-rating
max="1"
v-model="maxStars"
size="1.5em"
color="yellow"
icon="star"
icon-selected="star"
readonly
no-dimming
>
</q-rating>
</div> -->
</div>
</div>
</q-item-section>
</template>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
<q-expansion-item
v-for="release of selectedExtensionRepos[repoName].releases"
:key="release.version"
group="releases"
:icon="release.isInstalled ? 'download_done' : 'download'"
:label="release.description"
:caption="release.version"
:content-inset-level="0.5"
:header-class="release.isInstalled ? 'text-green' : ''"
>
<q-card>
<q-card-section>
<q-btn
v-if="!release.isInstalled"
@click="installExtension(release)"
color="primary unelevated mt-lg pt-lg"
>Install</q-btn
>
<q-btn v-else @click="showUninstall()" flat color="red">
Uninstall</q-btn
>
<a
v-if="release.html_url"
class="text-secondary float-right"
:href="release.html_url"
target="_blank"
style="color: inherit"
>Release Notes</a
>
</q-card-section>
<div
v-if="release.details_html"
v-html="release.details_html"
></div>
<q-separator></q-separator> </q-card
></q-expansion-item>
</q-list>
</q-card-section>
</q-expansion-item>
</q-card>
</div>
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
<div class="row q-mt-lg">
<q-btn
v-if="selectedExtension?.isInstalled"
@click="showUninstall()"
flat
color="red"
>
Uninstall</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
tab: 'all',
filteredExtensions: null,
showUninstallDialog: false,
showUpgradeDialog: false,
selectedExtension: null,
selectedExtensionRepos: null,
maxStars: 5
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
},
installExtension: async function (release) {
const extension = this.selectedExtension
extension.inProgress = true
this.showUpgradeDialog = false
LNbits.api
.request(
'POST',
`/api/v1/extension?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo
}
)
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showUpgradeDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
toggleExtension: function (extension) {
const action = extension.isActive ? 'activate' : 'deactivate'
LNbits.api
.request(
'GET',
"{{ url_for('install.extensions') }}?usr=" +
this.g.user.id +
'&' +
action +
'=' +
extension.id
)
.then(response => {})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
showUninstall: function () {
this.showUpgradeDialog = false
this.showUninstallDialog = true
},
showUpgrade: async function (extension) {
this.selectedExtension = extension
this.showUpgradeDialog = true
this.selectedExtensionRepos = null
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/${extension.id}/releases?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
this.selectedExtensionRepos = data.reduce((repos, release) => {
repos[release.source_repo] = repos[release.source_repo] || {
releases: [],
isInstalled: false
}
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
if (release.isInstalled) {
repos[release.source_repo].isInstalled = true
}
repos[release.source_repo].releases.push(release)
return repos
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
hasNewVersion: function (extension) {
if (extension.installedRelease && extension.latestRelease) {
return (
extension.installedRelease.version !==
extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
}
},
created: function () {
if (!this.g.user.admin) {
this.$q.notify({
timeout: 3000,
message: 'Only admin accounts can install extensions',
icon: null
})
}
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
...e,
inProgress: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
},
mixins: [windowMixin]
})
</script>
{% endblock %}
+24 -22
View File
@@ -125,7 +125,6 @@
</div>
</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
@@ -138,17 +137,18 @@
<q-table
dense
flat
:data="filteredPayments"
:data="payments"
:row-key="paymentTableRowKey"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
:no-data-label="$t('no_transactions')"
:filter="paymentsTable.filter"
:loading="paymentsTable.loading"
@request="fetchPayments"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"
>{{ col.label }}</q-th
>
@@ -191,15 +191,15 @@
</a>
</q-badge>
{{ props.row.memo }}
</q-td>
<q-td auto-width key="date" :props="props">
<br />
<q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }}
<i> {{ props.row.dateFrom }}</i>
</q-td>
{% endraw %}
<q-td
auto-width
key="sat"
key="amount"
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
:props="props"
>{% raw %} {{
@@ -207,11 +207,9 @@
}}
</q-td>
<q-td auto-width key="sat" v-else :props="props">
{{ props.row.fsat }}
</q-td>
<q-td auto-width key="fee" :props="props">
{{ props.row.fee }}
<q-td auto-width key="amount" v-else :props="props">
{{ props.row.fsat }}<br />
<i>fee {{ props.row.fee }}</i>
</q-td>
</q-tr>
@@ -551,7 +549,7 @@
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong v-text="$t('description')">:</strong> {{
<strong v-text="$t('memo')">:</strong> {{
parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
@@ -778,23 +776,27 @@
</q-card-section>
</q-card>
</q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
active-class="px-0"
indicator-color="transparent"
align="justify"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog">
</q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
<q-tab @click="showParseDialog" icon="file_upload" label="Send">
</q-tab>
<q-btn
round
size="35px"
unelevated
icon="qr_code_scanner"
@click="showCamera"
class="text-white bg-primary z-top vertical-bottom absolute-center absolute"
>
</q-btn>
</q-tabs>
<q-dialog v-model="disclaimerDialog.show">
+6 -3
View File
@@ -32,11 +32,14 @@ async def api_get_settings(
@core_app.put(
"/admin/api/v1/settings/",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def api_update_settings(data: EditableSettings):
async def api_update_settings(
data: EditableSettings, user: User = Depends(check_admin)
):
await update_admin_settings(data)
update_cached_settings(dict(data))
admin_settings = await get_admin_settings(user.super_user)
assert admin_settings, "Updated admin settings not found."
update_cached_settings(admin_settings.dict())
return {"status": "Success"}
+67 -8
View File
@@ -33,8 +33,8 @@ from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import Payment, User, Wallet
from lnbits.db import Filters
from lnbits.core.models import Payment, PaymentFilters, User, Wallet
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
@@ -48,6 +48,7 @@ from lnbits.extension_manager import (
Extension,
ExtensionRelease,
InstallableExtension,
fetch_github_release_config,
get_valid_extensions,
)
from lnbits.helpers import generate_filter_params_openapi, url_for
@@ -66,6 +67,7 @@ from ..crud import (
delete_tinyurl,
get_dbversions,
get_payments,
get_payments_paginated,
get_standalone_payment,
get_tinyurl,
get_tinyurl_by_url,
@@ -122,19 +124,19 @@ async def api_update_wallet(
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(Payment),
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(Payment)),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
pendingPayments = await get_payments(
pending_payments = await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
filters=filters,
)
for payment in pendingPayments:
for payment in pending_payments:
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
@@ -146,6 +148,37 @@ async def api_payments(
)
@core_app.get(
"/api/v1/payments/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_paginated(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
pending = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
filters=filters,
)
for payment in pending.data:
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
page = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
return page
class CreateInvoiceData(BaseModel):
out: Optional[bool] = True
amount: float = Query(None, ge=0)
@@ -747,6 +780,11 @@ async def api_install_extension(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
if not release.is_version_compatible:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
)
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
@@ -782,13 +820,12 @@ async def api_install_extension(
ext_info.clean_extension_files()
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to install extension.",
detail=f"Failed to install extension {ext_info.id} ({ext_info.installed_version}).",
)
@core_app.delete("/api/v1/extension/{ext_id}")
async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)):
installable_extensions = await InstallableExtension.get_installable_extensions()
extensions = [e for e in installable_extensions if e.id == ext_id]
@@ -820,6 +857,7 @@ async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)
ext_info.clean_extension_files()
await delete_installed_extension(ext_id=ext_info.id)
logger.success(f"Extension '{ext_id}' uninstalled.")
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
@@ -843,6 +881,27 @@ async def get_extension_releases(ext_id: str):
)
@core_app.get(
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
dependencies=[Depends(check_admin)],
)
async def get_extension_release(org: str, repo: str, tag_name: str):
try:
config = await fetch_github_release_config(org, repo, tag_name)
if not config:
return {}
return {
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
}
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
# TINYURL
+36 -33
View File
@@ -11,6 +11,7 @@ from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse
from lnbits.core import db
from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User
from lnbits.decorators import check_admin, check_user_exists
from lnbits.helpers import template_renderer, url_for
@@ -56,11 +57,13 @@ async def robots():
@core_html_routes.get(
"/extensions", name="core.extensions", response_class=HTMLResponse
"/extensions", name="install.extensions", response_class=HTMLResponse
)
async def extensions(
async def extensions_install(
request: Request,
user: User = Depends(check_user_exists),
activate: str = Query(None),
deactivate: str = Query(None),
enable: str = Query(None),
disable: str = Query(None),
):
@@ -69,21 +72,6 @@ async def extensions(
# Update user as his extensions have been updated
if enable or disable:
user = await get_user(user.id) # type: ignore
return template_renderer().TemplateResponse(
"core/extensions.html", {"request": request, "user": user.dict()}
)
@core_html_routes.get(
"/install", name="install.extensions", response_class=HTMLResponse
)
async def extensions_install(
request: Request,
user: User = Depends(check_user_exists),
activate: str = Query(None),
deactivate: str = Query(None),
):
try:
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
@@ -150,7 +138,7 @@ async def extensions_install(
)
return template_renderer().TemplateResponse(
"core/install.html",
"core/extensions.html",
{
"request": request,
"user": user.dict(),
@@ -186,8 +174,10 @@ async def wallet(
wallet_name = nme
if not user_id:
user = await get_user((await create_account()).id)
logger.info(f"Create user {user.id}") # type: ignore
new_user = await create_account()
user = await get_user(new_user.id)
assert user, "Newly created user has to exist."
logger.info(f"Create user {user.id}")
else:
user = await get_user(user_id)
if not user:
@@ -209,23 +199,23 @@ async def wallet(
user.super_user = True
if not wallet_id:
if user.wallets and not wallet_name: # type: ignore
wallet = user.wallets[0] # type: ignore
if user.wallets and not wallet_name:
wallet = user.wallets[0]
else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) # type: ignore
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" # type: ignore
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
logger.debug(
f"Access {'user '+ user.id + ' ' if user else ''} {'wallet ' + wallet_name if wallet_name else ''}"
)
userwallet = user.get_wallet(wallet_id) # type: ignore
userwallet = user.get_wallet(wallet_id)
if not userwallet:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
@@ -235,10 +225,10 @@ async def wallet(
"core/wallet.html",
{
"request": request,
"user": user.dict(), # type: ignore
"user": user.dict(),
"wallet": userwallet.dict(),
"service_fee": settings.lnbits_service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
)
@@ -294,18 +284,21 @@ async def lnurl_full_withdraw_callback(request: Request):
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
async def deletewallet(wal: str = Query(...), usr: str = Query(...)):
user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets] # type: ignore
if not user:
raise HTTPException(HTTPStatus.FORBIDDEN, "User not found.")
user_wallet_ids = [u.id for u in user.wallets]
if wal not in user_wallet_ids:
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
else:
await delete_wallet(user_id=user.id, wallet_id=wal) # type: ignore
await delete_wallet(user_id=user.id, wallet_id=wal)
user_wallet_ids.remove(wal)
logger.debug("Deleted wallet {wal} of user {user.id}")
if user_wallet_ids:
return RedirectResponse(
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]), # type: ignore
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]),
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@@ -328,7 +321,8 @@ async def lnurlwallet(request: Request):
async with db.connect() as conn:
account = await create_account(conn=conn)
user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn) # type: ignore
assert user, "Newly created user not found."
wallet = await create_wallet(user_id=user.id, conn=conn)
asyncio.create_task(
redeem_lnurl_withdraw(
@@ -341,7 +335,7 @@ async def lnurlwallet(request: Request):
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@@ -406,6 +400,15 @@ async def index(request: Request, user: User = Depends(check_admin)):
)
@core_html_routes.get("/uuidv4/{hex_value}")
async def hex_to_uuid4(hex_value: str):
try:
user_id = to_valid_user_id(hex_value).hex
return RedirectResponse(url=f"/wallet?usr={user_id}")
except Exception as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
async def toggle_extension(extension_to_enable, extension_to_disable, user_id):
if extension_to_enable and extension_to_disable:
raise HTTPException(
+230 -72
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import asyncio
import datetime
import os
@@ -5,10 +7,11 @@ import re
import time
from contextlib import asynccontextmanager
from enum import Enum
from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar
from sqlite3 import Row
from typing import Any, Generic, List, Literal, Optional, Type, TypeVar
from loguru import logger
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
@@ -19,6 +22,51 @@ POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"
if settings.lnbits_database_url:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
DB_TYPE = COCKROACH
else:
DB_TYPE = POSTGRES
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
register_type(
new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if os.path.isdir(settings.lnbits_data_folder):
DB_TYPE = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created"
f" - please 'mkdir {settings.lnbits_data_folder}' and try again"
)
class Compat:
type: Optional[str] = "<inherited>"
@@ -68,6 +116,16 @@ class Compat:
return "BIGINT"
return "INT"
@classmethod
@property
def timestamp_placeholder(cls):
if DB_TYPE == POSTGRES:
return "to_timestamp(?)"
elif DB_TYPE == COCKROACH:
return "cast(? AS timestamp)"
else:
return "?"
class Connection(Compat):
def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
@@ -87,17 +145,21 @@ class Connection(Compat):
# strip html
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
def cleanhtml(raw_html):
if isinstance(raw_html, str):
cleantext = re.sub(CLEANR, "", raw_html)
return cleantext
else:
return raw_html
# tuple to list and back to tuple
value_list = [values] if isinstance(values, str) else list(values)
values = tuple([cleanhtml(val) for val in value_list])
return values
raw_values = [values] if isinstance(values, str) else list(values)
values = []
for raw_value in raw_values:
if isinstance(raw_value, str):
values.append(re.sub(CLEANR, "", raw_value))
elif isinstance(raw_value, datetime.datetime):
ts = raw_value.timestamp()
if self.type == SQLITE:
values.append(int(ts))
else:
values.append(ts)
else:
values.append(raw_value)
return tuple(values)
async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute(
@@ -113,6 +175,51 @@ class Connection(Compat):
await result.close()
return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
if not filters:
filters = Filters()
clause = filters.where(where)
parsed_values = filters.values(values)
rows = await self.fetchall(
f"""
{query}
{clause}
{filters.order_by()}
{filters.pagination()}
""",
parsed_values,
)
if rows:
# no need for extra query if no pagination is specified
if filters.offset or filters.limit:
count = await self.fetchone(
f"""
SELECT COUNT(*) FROM (
{query}
{clause}
) as count
""",
parsed_values,
)
count = int(count[0])
else:
count = len(rows)
else:
count = 0
return Page(
data=[model.from_row(row) for row in rows] if model else rows,
total=count,
)
async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
@@ -122,57 +229,17 @@ class Connection(Compat):
class Database(Compat):
def __init__(self, db_name: str):
self.name = db_name
self.schema = self.name
self.type = DB_TYPE
if settings.lnbits_database_url:
if DB_TYPE == SQLITE:
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
else:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
self.type = COCKROACH
else:
self.type = POSTGRES
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
register_type(
new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if os.path.isdir(settings.lnbits_data_folder):
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created"
f" - please 'mkdir {settings.lnbits_data_folder}' and try again"
)
logger.trace(f"database {self.type} added for {self.name}")
self.schema = self.name
if self.name.startswith("ext_"):
self.schema = self.name[4:]
else:
@@ -181,6 +248,8 @@ class Database(Compat):
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager
async def connect(self):
await self.lock.acquire()
@@ -215,6 +284,17 @@ class Database(Compat):
await result.close()
return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
async with self.connect() as conn:
return await conn.fetch_page(query, where, values, filters, model)
async def execute(self, query: str, values: tuple = ()):
async with self.connect() as conn:
return await conn.execute(query, values)
@@ -229,6 +309,8 @@ class Operator(Enum):
LT = "lt"
EQ = "eq"
NE = "ne"
GE = "ge"
LE = "le"
INCLUDE = "in"
EXCLUDE = "ex"
@@ -246,21 +328,46 @@ class Operator(Enum):
return ">"
elif self == Operator.LT:
return "<"
elif self == Operator.GE:
return ">="
elif self == Operator.LE:
return "<="
else:
raise ValueError("Unknown SQL Operator")
class FromRowModel(BaseModel):
@classmethod
def from_row(cls, row: Row):
return cls(**dict(row))
class FilterModel(BaseModel):
__search_fields__: List[str] = []
__sort_fields__: Optional[List[str]] = None
T = TypeVar("T")
TModel = TypeVar("TModel", bound=BaseModel)
TRowModel = TypeVar("TRowModel", bound=FromRowModel)
TFilterModel = TypeVar("TFilterModel", bound=FilterModel)
class Filter(BaseModel, Generic[TModel]):
class Page(BaseModel, Generic[T]):
data: list[T]
total: int
class Filter(BaseModel, Generic[TFilterModel]):
field: str
nested: Optional[list[str]]
nested: Optional[List[str]]
op: Operator = Operator.EQ
values: list[Any]
model: Optional[Type[TFilterModel]]
@classmethod
def parse_query(cls, key: str, raw_values: list[Any], model: Type[TModel]):
def parse_query(cls, key: str, raw_values: list[Any], model: Type[TFilterModel]):
# Key format:
# key[operator]
# e.g. name[eq]
@@ -300,7 +407,7 @@ class Filter(BaseModel, Generic[TModel]):
else:
raise ValueError("Unknown filter field")
return cls(field=field, op=op, nested=nested, values=values)
return cls(field=field, op=op, nested=nested, values=values, model=model)
@property
def statement(self):
@@ -308,18 +415,49 @@ class Filter(BaseModel, Generic[TModel]):
if self.nested:
for name in self.nested:
accessor = f"({accessor} ->> '{name}')"
if self.model and self.model.__fields__[self.field].type_ == datetime.datetime:
placeholder = Compat.timestamp_placeholder
else:
placeholder = "?"
if self.op in (Operator.INCLUDE, Operator.EXCLUDE):
placeholders = ", ".join(["?"] * len(self.values))
placeholders = ", ".join([placeholder] * len(self.values))
stmt = [f"{accessor} {self.op.as_sql} ({placeholders})"]
else:
stmt = [f"{accessor} {self.op.as_sql} ?"] * len(self.values)
stmt = [f"{accessor} {self.op.as_sql} {placeholder}"] * len(self.values)
return " OR ".join(stmt)
class Filters(BaseModel, Generic[TModel]):
filters: List[Filter[TModel]] = []
limit: Optional[int]
offset: Optional[int]
class Filters(BaseModel, Generic[TFilterModel]):
"""
Generic helper class for filtering and sorting data.
For usage in an api endpoint, use the `parse_filters` dependency.
When constructing this class manually always make sure to pass a model so that the values can be validated.
Otherwise, make sure to validate the inputs manually.
"""
filters: List[Filter[TFilterModel]] = []
search: Optional[str] = None
offset: Optional[int] = None
limit: Optional[int] = None
sortby: Optional[str] = None
direction: Optional[Literal["asc", "desc"]] = None
model: Optional[Type[TFilterModel]] = None
@root_validator(pre=True)
def validate_sortby(cls, values):
sortby = values.get("sortby")
model = values.get("model")
if sortby and model:
model = values["model"]
# if no sort fields are specified explicitly all fields are allowed
allowed = model.__sort_fields__ or model.__fields__
if sortby not in allowed:
raise ValueError("Invalid sort field")
return values
def pagination(self) -> str:
stmt = ""
@@ -329,16 +467,36 @@ class Filters(BaseModel, Generic[TModel]):
stmt += f"OFFSET {self.offset}"
return stmt
def where(self, where_stmts: List[str]) -> str:
def where(self, where_stmts: Optional[List[str]] = None) -> str:
if not where_stmts:
where_stmts = []
if self.filters:
for filter in self.filters:
where_stmts.append(filter.statement)
if self.search and self.model:
if DB_TYPE == POSTGRES:
where_stmts.append(
f"lower(concat({f', '.join(self.model.__search_fields__)})) LIKE ?"
)
elif DB_TYPE == SQLITE:
where_stmts.append(
f"lower({'||'.join(self.model.__search_fields__)}) LIKE ?"
)
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
return ""
def values(self, values: List[str]) -> Tuple:
def order_by(self) -> str:
if self.sortby:
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
return ""
def values(self, values: Optional[List[str]] = None) -> tuple:
if not values:
values = []
if self.filters:
for filter in self.filters:
values.extend(filter.values)
if self.search and self.model:
values.append(f"%{self.search}%")
return tuple(values)
+15 -8
View File
@@ -1,16 +1,16 @@
from http import HTTPStatus
from typing import Optional, Type
from typing import Literal, Optional, Type
from fastapi import HTTPException, Request, Security, status
from fastapi import Query, Request, Security, status
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic import BaseModel
from pydantic.types import UUID4
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.db import Filter, Filters
from lnbits.db import Filter, Filters, TFilterModel
from lnbits.requestvars import g
from lnbits.settings import settings
@@ -185,7 +185,6 @@ async def require_admin_key(
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
if not token:
@@ -211,7 +210,6 @@ async def require_invoice_key(
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
if not token:
@@ -279,14 +277,19 @@ async def check_super_user(usr: UUID4) -> User:
return user
def parse_filters(model: Type[BaseModel]):
def parse_filters(model: Type[TFilterModel]):
"""
Parses the query params as filters.
:param model: model used for validation of filter values
"""
def dependency(
request: Request, limit: Optional[int] = None, offset: Optional[int] = None
request: Request,
limit: Optional[int] = None,
offset: Optional[int] = None,
sortby: Optional[str] = None,
direction: Optional[Literal["asc", "desc"]] = None,
search: Optional[str] = Query(None, description="Text based search"),
):
params = request.query_params
filters = []
@@ -300,6 +303,10 @@ def parse_filters(model: Type[BaseModel]):
filters=filters,
limit=limit,
offset=offset,
sortby=sortby,
direction=direction,
search=search,
model=model,
)
return dependency
+89 -27
View File
@@ -12,6 +12,7 @@ from urllib import request
import httpx
from fastapi import HTTPException
from loguru import logger
from packaging import version
from pydantic import BaseModel
from lnbits.settings import settings
@@ -26,11 +27,17 @@ class ExplicitRelease(BaseModel):
dependencies: List[str] = []
icon: Optional[str]
short_description: Optional[str]
html_url: Optional[str]
details: Optional[str]
min_lnbits_version: Optional[str]
html_url: Optional[str] # todo: release_url
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
def is_version_compatible(self):
if not self.min_lnbits_version:
return True
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
class GitHubRelease(BaseModel):
id: str
@@ -61,6 +68,13 @@ class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
warning: Optional[str] = ""
min_lnbits_version: Optional[str]
def is_version_compatible(self):
if not self.min_lnbits_version:
return True
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
def download_url(url, save_path):
@@ -117,6 +131,17 @@ async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
return [GitHubRepoRelease.parse_obj(r) for r in releases]
async def fetch_github_release_config(
org: str, repo: str, tag_name: str
) -> Optional[ExtensionConfig]:
config_url = (
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
)
error_msg = "Cannot fetch GitHub extension config"
config = await gihub_api_get(config_url, error_msg)
return ExtensionConfig.parse_obj(config)
async def gihub_api_get(url: str, error_msg: Optional[str]) -> Any:
async with httpx.AsyncClient() as client:
headers = (
@@ -180,7 +205,7 @@ class Extension(NamedTuple):
class ExtensionManager:
def __init__(self):
p = Path(settings.lnbits_path, "extensions")
os.makedirs(p, exist_ok=True)
Path(p).mkdir(parents=True, exist_ok=True)
self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
@property
@@ -224,9 +249,11 @@ class ExtensionRelease(BaseModel):
source_repo: str
is_github_release: bool = False
hash: Optional[str] = None
min_lnbits_version: Optional[str] = None
is_version_compatible: Optional[bool] = True
html_url: Optional[str] = None
description: Optional[str] = None
details_html: Optional[str] = None
warning: Optional[str] = None
icon: Optional[str] = None
@classmethod
@@ -244,6 +271,24 @@ class ExtensionRelease(BaseModel):
html_url=r.html_url,
)
@classmethod
def from_explicit_release(
cls, source_repo: str, e: "ExplicitRelease"
) -> "ExtensionRelease":
return ExtensionRelease(
name=e.name,
version=e.version,
archive=e.archive,
hash=e.hash,
source_repo=source_repo,
description=e.short_description,
min_lnbits_version=e.min_lnbits_version,
is_version_compatible=e.is_version_compatible(),
warning=e.warning,
html_url=e.html_url,
icon=e.icon,
)
@classmethod
async def all_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
try:
@@ -283,7 +328,7 @@ class InstallableExtension(BaseModel):
@property
def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "extensions")
os.makedirs(extensions_data_dir, exist_ok=True)
Path(extensions_data_dir).mkdir(parents=True, exist_ok=True)
return Path(extensions_data_dir, f"{self.id}.zip")
@property
@@ -308,8 +353,14 @@ class InstallableExtension(BaseModel):
return False
return Path(self.ext_dir, "config.json").is_file()
@property
def installed_version(self) -> str:
if self.installed_release:
return self.installed_release.version
return ""
def download_archive(self):
logger.info(f"Downloading extension {self.name}.")
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
ext_zip_file = self.zip_path
if ext_zip_file.is_file():
os.remove(ext_zip_file)
@@ -334,8 +385,8 @@ class InstallableExtension(BaseModel):
)
def extract_archive(self):
logger.info(f"Extracting extension {self.name}.")
os.makedirs(Path("lnbits", "upgrades"), exist_ok=True)
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
Path("lnbits", "upgrades").mkdir(parents=True, exist_ok=True)
shutil.rmtree(self.ext_upgrade_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(self.ext_upgrade_dir)
@@ -369,7 +420,7 @@ class InstallableExtension(BaseModel):
Path(self.ext_upgrade_dir, self.id),
Path(settings.lnbits_path, "extensions", self.id),
)
logger.success(f"Extension {self.name} installed.")
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
def nofiy_upgrade(self) -> None:
"""Update the list of upgraded extensions. The middleware will perform redirects based on this"""
@@ -394,6 +445,15 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_upgrade_dir, True)
def check_latest_version(self, release: Optional[ExtensionRelease]):
if not release:
return
if not self.latest_release:
self.latest_release = release
return
if version.parse(self.latest_release.version) < version.parse(release.version):
self.latest_release = release
@classmethod
def from_row(cls, data: dict) -> "InstallableExtension":
meta = json.loads(data["meta"])
@@ -451,18 +511,30 @@ class InstallableExtension(BaseModel):
manifest = await fetch_manifest(url)
for r in manifest.repos:
if r.id in extension_id_list:
continue
ext = await InstallableExtension.from_github_release(r)
if ext:
ext.featured = ext.id in manifest.featured
extension_list += [ext]
extension_id_list += [ext.id]
if not ext:
continue
existing_ext = next(
(ee for ee in extension_list if ee.id == r.id), None
)
if existing_ext:
existing_ext.check_latest_version(ext.latest_release)
continue
ext.featured = ext.id in manifest.featured
extension_list += [ext]
extension_id_list += [ext.id]
for e in manifest.extensions:
if e.id in extension_id_list:
release = ExtensionRelease.from_explicit_release(url, e)
existing_ext = next(
(ee for ee in extension_list if ee.id == e.id), None
)
if existing_ext:
existing_ext.check_latest_version(release)
continue
ext = InstallableExtension.from_explicit_release(e)
ext.check_latest_version(release)
ext.featured = ext.id in manifest.featured
extension_list += [ext]
extension_id_list += [e.id]
@@ -488,17 +560,7 @@ class InstallableExtension(BaseModel):
for e in manifest.extensions:
if e.id == ext_id:
extension_releases += [
ExtensionRelease(
name=e.name,
version=e.version,
archive=e.archive,
hash=e.hash,
source_repo=url,
description=e.short_description,
details_html=e.details,
html_url=e.html_url,
icon=e.icon,
)
ExtensionRelease.from_explicit_release(url, e)
]
except Exception as e:
+19 -30
View File
@@ -1,8 +1,9 @@
import json
from pathlib import Path
from typing import Any, List, Optional, Type
import jinja2
import shortuuid
from pydantic import BaseModel
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
@@ -13,29 +14,9 @@ from lnbits.jinja2_templating import Jinja2Templates
from lnbits.requestvars import g
from lnbits.settings import settings
from .db import FilterModel
from .extension_manager import get_valid_extensions
vendored_js = [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vue-i18n.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
]
vendored_css = [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/vendor/vue-qrcode-reader.css",
]
def urlsafe_short_hash() -> str:
return shortuuid.uuid()
@@ -51,7 +32,6 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders:
folders.extend(additional_folders)
@@ -68,7 +48,8 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
t.env.globals["LNBITS_VERSION"] = settings.lnbits_commit
t.env.globals["COMMIT_VERSION"] = settings.lnbits_commit
t.env.globals["LNBITS_VERSION"] = settings.version
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
t.env.globals["EXTENSIONS"] = [
e
@@ -78,12 +59,15 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
if settings.lnbits_custom_logo:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
if settings.debug:
t.env.globals["VENDORED_JS"] = vendored_js
t.env.globals["VENDORED_CSS"] = vendored_css
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["/static/bundle.min.js"]
t.env.globals["INCLUDED_CSS"] = ["/static/bundle.min.css"]
else:
t.env.globals["VENDORED_JS"] = ["/static/bundle.js"]
t.env.globals["VENDORED_CSS"] = ["/static/bundle.css"]
vendor_filepath = Path(settings.lnbits_path, "static", "vendor.json")
with open(vendor_filepath) as vendor_file:
vendor_files = json.loads(vendor_file.read())
t.env.globals["INCLUDED_JS"] = vendor_files["js"]
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
return t
@@ -111,7 +95,7 @@ def get_current_extension_name() -> str:
return ext_name
def generate_filter_params_openapi(model: Type[BaseModel], keep_optional=False):
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
"""
Generate openapi documentation for Filters. This is intended to be used along parse_filters (see example)
:param model: Filter model
@@ -132,6 +116,11 @@ def generate_filter_params_openapi(model: Type[BaseModel], keep_optional=False):
description = "Supports Filtering"
if schema["type"] == "object":
description += f". Nested attributes can be filtered too, e.g. `{field.alias}.[additional].[attributes]`"
if (
hasattr(model, "__search_fields__")
and field.name in model.__search_fields__
):
description += ". Supports Search"
parameter = {
"name": field.alias,
+4
View File
@@ -4,6 +4,7 @@ uvloop.install()
import multiprocessing as mp
import time
from pathlib import Path
import click
import uvicorn
@@ -37,6 +38,9 @@ def main(
):
"""Launched with `poetry run lnbits` at root level"""
# create data dir if it does not exist
Path(settings.lnbits_data_folder).mkdir(parents=True, exist_ok=True)
set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips)
# this beautiful beast parses all command line arguments and passes them to the uvicorn server
+6 -1
View File
@@ -50,6 +50,9 @@ class ExtensionsSettings(LNbitsSettings):
"https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
]
)
class ExtensionsInstallSettings(LNbitsSettings):
lnbits_extensions_default_install: List[str] = Field(default=[])
# required due to GitHUb rate-limit
lnbits_ext_github_token: str = Field(default="")
@@ -227,6 +230,7 @@ class EditableSettings(
class EnvSettings(LNbitsSettings):
debug: bool = Field(default=False)
bundle_assets: bool = Field(default=True)
host: str = Field(default="127.0.0.1")
port: int = Field(default=5000)
forwarded_allow_ips: str = Field(default="*")
@@ -278,6 +282,7 @@ class TransientSettings(InstalledExtensionsSettings):
class ReadOnlySettings(
EnvSettings,
ExtensionsInstallSettings,
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
@@ -309,7 +314,7 @@ class SuperSettings(EditableSettings):
class AdminSettings(EditableSettings):
super_user: bool
is_super_user: bool
lnbits_allowed_funding_sources: Optional[List[str]]
+1
View File
File diff suppressed because one or more lines are too long
+55
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
base.css
+545
View File
@@ -0,0 +1,545 @@
[data-theme=classic] .q-drawer--dark,
body[data-theme=classic].body--dark,
[data-theme=classic] .q-menu--dark {
background: #1f2234 !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='classic'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=classic] .q-card--dark,
[data-theme=classic] .q-stepper--dark {
background: #333646 !important;
}
[data-theme=classic] .bg-primary {
background: #673ab7 !important;
}
[data-theme=classic] .text-primary {
color: #673ab7 !important;
}
[data-theme=classic] .bg-secondary {
background: #9c27b0 !important;
}
[data-theme=classic] .text-secondary {
color: #9c27b0 !important;
}
[data-theme=classic] .bg-dark {
background: #1f2234 !important;
}
[data-theme=classic] .text-dark {
color: #1f2234 !important;
}
[data-theme=classic] .bg-info {
background: #333646 !important;
}
[data-theme=classic] .text-info {
color: #333646 !important;
}
[data-theme=classic] .bg-marginal-bg {
background: #1f2234 !important;
}
[data-theme=classic] .text-marginal-bg {
color: #1f2234 !important;
}
[data-theme=classic] .bg-marginal-text {
background: #fff !important;
}
[data-theme=classic] .text-marginal-text {
color: #fff !important;
}
[data-theme=bitcoin] .q-drawer--dark,
body[data-theme=bitcoin].body--dark,
[data-theme=bitcoin] .q-menu--dark {
background: #2d293b !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='bitcoin'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=bitcoin] .q-card--dark,
[data-theme=bitcoin] .q-stepper--dark {
background: #333646 !important;
}
[data-theme=bitcoin] .bg-primary {
background: #ff9853 !important;
}
[data-theme=bitcoin] .text-primary {
color: #ff9853 !important;
}
[data-theme=bitcoin] .bg-secondary {
background: #ff7353 !important;
}
[data-theme=bitcoin] .text-secondary {
color: #ff7353 !important;
}
[data-theme=bitcoin] .bg-dark {
background: #2d293b !important;
}
[data-theme=bitcoin] .text-dark {
color: #2d293b !important;
}
[data-theme=bitcoin] .bg-info {
background: #333646 !important;
}
[data-theme=bitcoin] .text-info {
color: #333646 !important;
}
[data-theme=bitcoin] .bg-marginal-bg {
background: #2d293b !important;
}
[data-theme=bitcoin] .text-marginal-bg {
color: #2d293b !important;
}
[data-theme=bitcoin] .bg-marginal-text {
background: #fff !important;
}
[data-theme=bitcoin] .text-marginal-text {
color: #fff !important;
}
[data-theme=freedom] .q-drawer--dark,
body[data-theme=freedom].body--dark,
[data-theme=freedom] .q-menu--dark {
background: #0a0a0a !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='freedom'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=freedom] .q-card--dark,
[data-theme=freedom] .q-stepper--dark {
background: #1b1b1b !important;
}
[data-theme=freedom] .bg-primary {
background: #e22156 !important;
}
[data-theme=freedom] .text-primary {
color: #e22156 !important;
}
[data-theme=freedom] .bg-secondary {
background: #b91a45 !important;
}
[data-theme=freedom] .text-secondary {
color: #b91a45 !important;
}
[data-theme=freedom] .bg-dark {
background: #0a0a0a !important;
}
[data-theme=freedom] .text-dark {
color: #0a0a0a !important;
}
[data-theme=freedom] .bg-info {
background: #1b1b1b !important;
}
[data-theme=freedom] .text-info {
color: #1b1b1b !important;
}
[data-theme=freedom] .bg-marginal-bg {
background: #2d293b !important;
}
[data-theme=freedom] .text-marginal-bg {
color: #2d293b !important;
}
[data-theme=freedom] .bg-marginal-text {
background: #fff !important;
}
[data-theme=freedom] .text-marginal-text {
color: #fff !important;
}
[data-theme=cyber] .q-drawer--dark,
body[data-theme=cyber].body--dark,
[data-theme=cyber] .q-menu--dark {
background: #0a0a0a !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='cyber'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=cyber] .q-card--dark,
[data-theme=cyber] .q-stepper--dark {
background: #1b1b1b !important;
}
[data-theme=cyber] .bg-primary {
background: #7cb342 !important;
}
[data-theme=cyber] .text-primary {
color: #7cb342 !important;
}
[data-theme=cyber] .bg-secondary {
background: #558b2f !important;
}
[data-theme=cyber] .text-secondary {
color: #558b2f !important;
}
[data-theme=cyber] .bg-dark {
background: #0a0a0a !important;
}
[data-theme=cyber] .text-dark {
color: #0a0a0a !important;
}
[data-theme=cyber] .bg-info {
background: #1b1b1b !important;
}
[data-theme=cyber] .text-info {
color: #1b1b1b !important;
}
[data-theme=cyber] .bg-marginal-bg {
background: #2d293b !important;
}
[data-theme=cyber] .text-marginal-bg {
color: #2d293b !important;
}
[data-theme=cyber] .bg-marginal-text {
background: #fff !important;
}
[data-theme=cyber] .text-marginal-text {
color: #fff !important;
}
[data-theme=mint] .q-drawer--dark,
body[data-theme=mint].body--dark,
[data-theme=mint] .q-menu--dark {
background: #1f342b !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='mint'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=mint] .q-card--dark,
[data-theme=mint] .q-stepper--dark {
background: #334642 !important;
}
[data-theme=mint] .bg-primary {
background: #3ab77d !important;
}
[data-theme=mint] .text-primary {
color: #3ab77d !important;
}
[data-theme=mint] .bg-secondary {
background: #27b065 !important;
}
[data-theme=mint] .text-secondary {
color: #27b065 !important;
}
[data-theme=mint] .bg-dark {
background: #1f342b !important;
}
[data-theme=mint] .text-dark {
color: #1f342b !important;
}
[data-theme=mint] .bg-info {
background: #334642 !important;
}
[data-theme=mint] .text-info {
color: #334642 !important;
}
[data-theme=mint] .bg-marginal-bg {
background: #1f342b !important;
}
[data-theme=mint] .text-marginal-bg {
color: #1f342b !important;
}
[data-theme=mint] .bg-marginal-text {
background: #fff !important;
}
[data-theme=mint] .text-marginal-text {
color: #fff !important;
}
[data-theme=autumn] .q-drawer--dark,
body[data-theme=autumn].body--dark,
[data-theme=autumn] .q-menu--dark {
background: #34291f !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='autumn'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=autumn] .q-card--dark,
[data-theme=autumn] .q-stepper--dark {
background: #463f33 !important;
}
[data-theme=autumn] .bg-primary {
background: #b7763a !important;
}
[data-theme=autumn] .text-primary {
color: #b7763a !important;
}
[data-theme=autumn] .bg-secondary {
background: #b07927 !important;
}
[data-theme=autumn] .text-secondary {
color: #b07927 !important;
}
[data-theme=autumn] .bg-dark {
background: #34291f !important;
}
[data-theme=autumn] .text-dark {
color: #34291f !important;
}
[data-theme=autumn] .bg-info {
background: #463f33 !important;
}
[data-theme=autumn] .text-info {
color: #463f33 !important;
}
[data-theme=autumn] .bg-marginal-bg {
background: #342a1f !important;
}
[data-theme=autumn] .text-marginal-bg {
color: #342a1f !important;
}
[data-theme=autumn] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=autumn] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
[data-theme=flamingo] .q-drawer--dark,
body[data-theme=flamingo].body--dark,
[data-theme=flamingo] .q-menu--dark {
background: #803a45 !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='flamingo'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=flamingo] .q-card--dark,
[data-theme=flamingo] .q-stepper--dark {
background: #ec7599 !important;
}
[data-theme=flamingo] .bg-primary {
background: #d11d53 !important;
}
[data-theme=flamingo] .text-primary {
color: #d11d53 !important;
}
[data-theme=flamingo] .bg-secondary {
background: #db3e6d !important;
}
[data-theme=flamingo] .text-secondary {
color: #db3e6d !important;
}
[data-theme=flamingo] .bg-dark {
background: #803a45 !important;
}
[data-theme=flamingo] .text-dark {
color: #803a45 !important;
}
[data-theme=flamingo] .bg-info {
background: #ec7599 !important;
}
[data-theme=flamingo] .text-info {
color: #ec7599 !important;
}
[data-theme=flamingo] .bg-marginal-bg {
background: #803a45 !important;
}
[data-theme=flamingo] .text-marginal-bg {
color: #803a45 !important;
}
[data-theme=flamingo] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=flamingo] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
[data-theme=monochrome] .q-drawer--dark,
body[data-theme=monochrome].body--dark,
[data-theme=monochrome] .q-menu--dark {
background: #000 !important;
}
/* IF WANTING TO SET A DARKER BG COLOR IN THE FUTURE
// set a darker body bg for all themes, when in "dark mode"
body[data-theme='monochrome'].body--dark {
background: scale-color($color, $lightness: -60%);
}
*/
[data-theme=monochrome] .q-card--dark,
[data-theme=monochrome] .q-stepper--dark {
background: rgb(39, 39, 39) !important;
}
[data-theme=monochrome] .bg-primary {
background: #494949 !important;
}
[data-theme=monochrome] .text-primary {
color: #494949 !important;
}
[data-theme=monochrome] .bg-secondary {
background: #6b6b6b !important;
}
[data-theme=monochrome] .text-secondary {
color: #6b6b6b !important;
}
[data-theme=monochrome] .bg-dark {
background: #000 !important;
}
[data-theme=monochrome] .text-dark {
color: #000 !important;
}
[data-theme=monochrome] .bg-info {
background: rgb(39, 39, 39) !important;
}
[data-theme=monochrome] .text-info {
color: rgb(39, 39, 39) !important;
}
[data-theme=monochrome] .bg-marginal-bg {
background: #000 !important;
}
[data-theme=monochrome] .text-marginal-bg {
color: #000 !important;
}
[data-theme=monochrome] .bg-marginal-text {
background: rgb(255, 255, 255) !important;
}
[data-theme=monochrome] .text-marginal-text {
color: rgb(255, 255, 255) !important;
}
[data-theme=freedom] .q-drawer--dark {
background: #0a0a0a !important;
}
[data-theme=freedom] .q-header {
background: #0a0a0a !important;
}
[data-theme=cyber] .q-drawer--dark {
background: #0a0a0a !important;
}
[data-theme=cyber] .q-header {
background: #0a0a0a !important;
}
[data-theme=salvador] .q-drawer--dark {
background: #242424 !important;
}
[data-theme=salvador] .q-header {
background: #0f47af !important;
}
[data-theme=flamingo] .q-drawer--dark {
background: #e75480 !important;
}
[data-theme=flamingo] .q-header {
background: #e75480 !important;
}
[v-cloak] {
display: none;
}
body.body--dark .q-table--dark {
background: transparent;
}
body.body--dark .q-field--error .text-negative,
body.body--dark .q-field--error .q-field__messages {
color: yellow !important;
}
.lnbits-drawer__q-list .q-item {
padding-top: 5px !important;
padding-bottom: 5px !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.lnbits-drawer__q-list .q-item.q-item--active {
color: inherit;
font-weight: bold;
}
.lnbits__dialog-card {
width: 500px;
}
.q-table--dense th:first-child,
.q-table--dense td:first-child,
.q-table--dense .q-table__bottom {
padding-left: 6px !important;
}
.q-table--dense th:last-child,
.q-table--dense td:last-child,
.q-table--dense .q-table__bottom {
padding-right: 6px !important;
}
a.inherit {
color: inherit;
text-decoration: none;
}
video {
border-radius: 3px;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url(/static/fonts/material-icons-v50.woff2) format("woff2");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: "liga";
-moz-osx-font-smoothing: grayscale;
}
.q-rating__icon {
font-size: 1em;
}
.text-wrap {
word-break: break-word;
}
.q-card code {
overflow-wrap: break-word;
}
+88
View File
@@ -0,0 +1,88 @@
window.localisation.br = {
server: 'Servidor',
theme: 'Tema',
funding: 'Financiamento',
users: 'Usuários',
restart: 'Reiniciar servidor',
save: 'Salvar',
save_tooltip: 'Salvar suas alterações',
topup: 'Recarregar',
topup_wallet: 'Recarregar uma carteira',
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
reset_defaults: 'Redefinir para padrões',
reset_defaults_tooltip:
'Apagar todas as configurações e redefinir para os padrões.',
download_backup: 'Fazer backup do banco de dados',
name_your_wallet: 'Nomeie sua carteira %{name}',
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
lnbits_description:
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
export_to_phone: 'Exportar para o telefone com código QR',
export_to_phone_desc:
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
wallets: 'Carteiras',
add_wallet: 'Adicionar nova carteira',
delete_wallet: 'Excluir carteira',
delete_wallet_desc:
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
rename_wallet: 'Renomear carteira',
update_name: 'Atualizar nome',
press_to_claim: 'Pressione para solicitar bitcoin',
donate: 'Doar',
view_github: 'Ver no GitHub',
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
toggle_darkmode: 'Alternar modo escuro',
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
api_docs: 'Documentação da API',
commit_version: 'Versão de commit',
lnbits_version: 'Versão do LNbits',
runs_on: 'Executa em',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '%{denomination} para creditar',
paste_request: 'Colar Pedido',
create_invoice: 'Criar Fatura',
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
export_csv: 'Exportar para CSV',
transactions: 'Transações',
chart_tooltip: 'Mostrar gráfico',
pending: 'Pendente',
copy_invoice: 'Copiar fatura',
close: 'Fechar',
cancel: 'Cancelar',
scan: 'Escanear',
read: 'Ler',
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
processing_payment: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',
payment_received: 'Pagamento Recebido',
payment_sent: 'Pagamento Enviado',
outgoing_payment_pending: 'Pagamento pendente de saída',
drain_funds: 'Drenar Fundos',
drain_funds_desc:
'Este é um código QR de retirada do LNURL para sugar tudo desta carteira. Não compartilhe com ninguém. É compatível com balanceCheck e balanceNotify para que sua carteira possa continuar retirando os fundos continuamente daqui após a primeira retirada.',
i_understand: 'Eu entendo',
copy_wallet_url: 'Copiar URL da carteira',
disclaimer_dialog:
'Funcionalidade de login a ser lançada em uma atualização futura, por enquanto, certifique-se de marcar esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
no_transactions: 'Ainda não foram feitas transações',
manage_extensions: 'Gerenciar extensões',
manage_server: 'Gerenciar servidor',
extensions: 'Extensões',
no_extensions: 'Você não possui nenhuma extensão instalada :(',
created: 'Criado',
payment_hash: 'Hash de pagamento',
fee: 'Taxa',
amount: 'Quantidade',
unit: 'Unidade',
description: 'Descrição',
expiry: 'Validade',
webhook: 'Webhook',
payment_proof: 'Comprovante de pagamento'
}
+63 -30
View File
@@ -9,46 +9,46 @@ window.localisation.de = {
save_tooltip: 'Änderungen speichern',
topup: 'Aufladen',
topup_wallet: 'Wallet aufladen',
topup_hint: 'Nutze die Wallet-ID um eine beliebige Wallet aufzuladen',
restart_tooltip: 'Starte den Server neu um die Änderungen zu übernehmen',
topup_hint: 'Nutze die Wallet-ID, um eine beliebige Wallet aufzuladen',
restart_tooltip: 'Starte den Server neu, um die Änderungen zu übernehmen',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen',
reset_defaults_tooltip:
'Lösche alle Einstellungen und setze auf die Standardeinstellungen zurück.',
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Benenne deine %{name} Wallet',
name_your_wallet: 'Vergib deiner %{name} Wallet einen Namen',
paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder lnurl ein *',
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
lnbits_description:
'Einfach zu installieren und leichtgewichtig, LNbits kann auf jeder Lightning-Netzwerk-Funding-Quelle laufen, derzeit unterstützt LND, c-lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst laufen lassen oder anderen eine kundige Lösung anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und es gibt keine Begrenzung für die Anzahl der Wallets, die du erstellen kannst. Die Möglichkeit, Gelder zu partitionieren, macht LNbits zu einem nützlichen Werkzeug für die Geldverwaltung und als Entwicklungswerkzeug. Erweiterungen fügen der LNbits zusätzliche Funktionalität hinzu, so dass du mit einer Reihe von vorderen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt ermutigen wir Menschen, sich selbst zu entwickeln und ihre eigenen Beiträge einzureichen.',
export_to_phone: 'Exportieren Sie auf das Telefon mit QR-Code',
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
export_to_phone: 'Auf dem Telefon öffnen',
export_to_phone_desc:
'Dieser QR-Code enthält Ihre Wallet-URL mit vollständigem Zugriff. Sie können es von Ihrem Telefon aus scannen, um Ihre Wallet dort zu öffnen.',
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
wallets: 'Wallets',
add_wallet: 'Füge eine neue Wallet hinzu',
add_wallet: 'Wallet hinzufügen',
delete_wallet: 'Wallet löschen',
delete_wallet_desc:
'Diese ganze Wallet wird gelöscht, die Mittel sind UNWIEDERBRINGLICH.',
'Die Wallet wird gelöscht, die hierin beinhalteten Daten hierin oder innerhalb einer Erweiterung sind UNWIEDERBRINGLICH.',
rename_wallet: 'Wallet umbenennen',
update_name: 'Name aktualisieren',
press_to_claim: 'Drücken Sie, um Bitcoin zu beanspruchen',
update_name: 'Namen aktualisieren',
press_to_claim: 'Klicken, um Bitcoin einzufordern.',
donate: 'Spenden',
view_github: 'Auf GitHub anzeigen',
voidwallet_active: 'VoidWallet ist aktiv! Zahlungen deaktiviert',
use_with_caution:
'VERWENDEN SIE MIT VORSICHT - %{name} Wallet ist noch in der BETA',
toggle_darkmode: 'Dark Mode umschalten',
view_swagger_docs: 'LNbits Swagger API-Dokumente anzeigen',
api_docs: 'Api docs',
commit_version: 'Commit version',
'BITTE MIT VORSICHT BENUTZEN - %{name} Wallet ist noch BETA',
toggle_darkmode: 'Auf Dark Mode umschalten',
view_swagger_docs: 'LNbits Swagger API-Dokumente',
api_docs: 'API docs',
commit_version: 'Commit Version',
runs_on: 'Läuft auf',
credit_hint: 'Drücken Sie Enter, um das Konto zu belasten',
credit_hint: 'Klicke Enter, um das Konto zu belasten',
credit_label: '%{denomination} zu belasten',
paste_request: 'Anfrage einfügen',
create_invoice: 'Rechnung erstellen',
camera_tooltip:
'Verwenden Sie die Kamera, um eine Rechnung / QR-Code zu scannen',
export_csv: 'Exportieren Sie nach CSV',
'Verwende die Kamera, um eine Rechnung oder einen QR-Code zu scannen',
export_csv: 'Exportieren als CSV',
transactions: 'Transaktionen',
chart_tooltip: 'Diagramm anzeigen',
pending: 'Ausstehend',
@@ -60,31 +60,64 @@ window.localisation.de = {
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Zahlung wird bearbeitet ...',
not_enough_funds: 'Nicht genug Geld!',
processing_payment: 'Zahlung wird verarbeitet ...',
not_enough_funds: 'Geldmittel sind erschöpft!',
search_by_tag_memo_amount: 'Suche nach Tag, Memo, Betrag',
invoice_waiting: 'Rechnung wartet auf Zahlung',
invoice_waiting: 'Rechnung wartend auf Zahlung',
payment_received: 'Zahlung erhalten',
payment_sent: 'Zahlung gesendet',
outgoing_payment_pending: 'Ausgehende Zahlung ausstehend',
drain_funds: 'Leeren Sie die Mittel',
outgoing_payment_pending: 'Ausgehende Zahlung wartend',
drain_funds: 'Sats abziehen',
drain_funds_desc:
'Dies ist ein LNURL-withdraw QR-Code zum Absaugen aller Mittel aus dieser Brieftasche. Teilen Sie es nicht mit jemandem. Es ist mit balanceCheck und balanceNotify kompatibel, so dass Ihre Brieftasche die Mittel nach dem ersten Abzug kontinuierlich von hier ziehen kann.',
'LNURL-withdraw QR-Code, der das Abziehen aller Geldmittel aus dieser Wallet erlaubt. Teile ihn mit niemandem! Kompatibel mit balanceCheck und balanceNotify, so dass dein Wallet die Sats nach dem ersten Abzug kontinuierlich von hier abziehen kann.',
i_understand: 'Ich verstehe',
copy_wallet_url: 'Wallet-URL kopieren',
disclaimer_dialog:
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht, für jetzt, stellen Sie sicher, dass Sie diese Seite als Lesezeichen speichern, um zukünftigen Zugriff auf Ihre Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Personen, die den Zugriff auf ihre Mittel verlieren.',
no_transactions: 'Noch keine Transaktionen',
'Login-Funktionalität wird in einem zukünftigen Update veröffentlicht. Bis dahin ist die Speicherung der Wallet-URL als Lesezeichen absolut notwendig, um Zugriff auf die Wallet zu erhalten! Dieser Service ist in BETA und wir übernehmen keine Verantwortung für Verluste durch verlorene Zugriffe.',
no_transactions: 'Keine Transaktionen',
manage_extensions: 'Erweiterungen verwalten',
manage_server: 'Server verwalten',
extensions: 'Erweiterungen',
no_extensions: 'Sie haben keine Erweiterungen installiert :(',
no_extensions: 'Du hast noch keine Erweiterungen installiert :(',
created: 'Erstellt',
search_extensions: 'Sucherweiterungen',
warning: 'Warnung',
manage: 'Verwalten',
repository: 'Repository',
confirm_continue: 'Bist du sicher, dass du fortfahren möchtest?',
manage_extension_details: 'Erweiterung installieren/deinstallieren',
install: 'Installieren',
uninstall: 'Deinstallieren',
open: 'Öffnen',
enable: 'Aktivieren',
enable_extension_details: 'Erweiterung für aktuellen Benutzer aktivieren',
disable: 'Deaktivieren',
installed: 'Installiert',
activated: 'Aktiviert',
deactivated: 'Deaktiviert',
release_notes: 'Versionshinweise',
activate_extension_details:
'Erweiterung für Benutzer verfügbar/nicht verfügbar machen',
featured: 'Vorgestellt',
all: 'Alle',
only_admins_can_install:
'(Nur Administratorkonten können Erweiterungen installieren)',
new_version: 'Neue Version',
extension_depends_on: 'Hängt ab von:',
extension_rating_soon: 'Bewertungen sind bald verfügbar',
extension_installed_version: 'Installierte Version',
extension_uninstall_warning:
'Sie sind dabei, die Erweiterung für alle Benutzer zu entfernen.',
uninstall_confirm: 'Ja, deinstallieren',
extension_min_lnbits_version:
'Diese Version erfordert mindestens die LNbits-Version',
payment_hash: 'Zahlungs-Hash',
fee: 'Gebühr',
amount: 'Menge',
description: 'Beschreibung',
expiry: 'Ablauf',
webhook: 'Webhook',
payment_proof: 'Zahlungsbeleg'
payment_proof: 'Beleg'
}
+32 -2
View File
@@ -17,7 +17,7 @@ window.localisation.en = {
name_your_wallet: 'Name your %{name} wallet',
paste_invoice_label: 'Paste an invoice, payment request or lnurl code *',
lnbits_description:
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, c-lightning, OpenNode, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
'Easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! You can run LNbits for yourself, or easily offer a custodian solution for others. Each wallet has its own API keys and there is no limit to the number of wallets you can make. Being able to partition funds makes LNbits a useful tool for money management and as a development tool. Extensions add extra functionality to LNbits so you can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage people to develop and submit their own.',
export_to_phone: 'Export to Phone with QR Code',
export_to_phone_desc:
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
@@ -37,6 +37,7 @@ window.localisation.en = {
view_swagger_docs: 'View LNbits Swagger API docs',
api_docs: 'Api docs',
commit_version: 'Commit version',
lnbits_version: 'LNbits version',
runs_on: 'Runs on',
credit_hint: 'Press Enter to credit account',
credit_label: '%{denomination} to credit',
@@ -70,11 +71,40 @@ window.localisation.en = {
disclaimer_dialog:
'Login functionality to be released in a future update, for now, make sure you bookmark this page for future access to your wallet! This service is in BETA, and we hold no responsibility for people losing access to funds.',
no_transactions: 'No transactions made yet',
manage_extensions: 'Manage Extensions',
manage_server: 'Manage Server',
extensions: 'Extensions',
no_extensions: "You don't have any extensions installed :(",
created: 'Created',
search_extensions: 'Search extensions',
warning: 'Warning',
manage: 'Manage',
repository: 'Repository',
confirm_continue: 'Are you sure you want to continue?',
manage_extension_details: 'Install/uninstall extension',
install: 'Install',
uninstall: 'Uninstall',
open: 'Open',
enable: 'Enable',
enable_extension_details: 'Enable extension for current user',
disable: 'Disable',
installed: 'Installed',
activated: 'Activated',
deactivated: 'Deactivated',
release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured',
all: 'All',
only_admins_can_install: '(Only admin accounts can install extensions)',
new_version: 'New Version',
extension_depends_on: 'Depends on:',
extension_rating_soon: 'Ratings coming soon',
extension_installed_version: 'Installed version',
extension_uninstall_warning:
'You are about to remove the extension for all users.',
uninstall_confirm: 'Yes, Uninstall',
extension_min_lnbits_version: 'This release requires at least LNbits version',
payment_hash: 'Payment Hash',
fee: 'Fee',
amount: 'Amount',
+34 -2
View File
@@ -19,7 +19,7 @@ window.localisation.es = {
name_your_wallet: 'Nombre de su billetera %{name}',
paste_invoice_label: 'Pegue la factura aquí',
lnbits_description:
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, c-lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
'Fácil de instalar y liviano, LNbits puede ejecutarse en cualquier fuente de financiación de la red Lightning, actualmente compatible con LND, Core Lightning, OpenNode, LNPay y hasta LNbits mismo! Puede ejecutar LNbits para usted mismo o ofrecer una solución competente a otros. Cada billetera tiene su propia clave API y no hay límite para la cantidad de billeteras que puede crear. La capacidad de particionar fondos hace de LNbits una herramienta útil para la administración de fondos y como herramienta de desarrollo. Las extensiones agregan funcionalidad adicional a LNbits, por lo que puede experimentar con una variedad de tecnologías de vanguardia en la red Lightning. Lo hemos hecho lo más simple posible para desarrollar extensiones y, como un proyecto gratuito y de código abierto, animamos a las personas a que se desarrollen a sí mismas y envíen sus propios contribuciones.',
export_to_phone: 'Exportar a teléfono con código QR',
export_to_phone_desc:
'Este código QR contiene su URL de billetera con acceso completo. Puede escanearlo desde su teléfono para abrir su billetera allí.',
@@ -72,11 +72,43 @@ window.localisation.es = {
disclaimer_dialog:
'La funcionalidad de inicio de sesión se lanzará en una actualización futura, por ahora, asegúrese de guardar esta página como marcador para acceder a su billetera en el futuro. Este servicio está en BETA y no asumimos ninguna responsabilidad por personas que pierdan el acceso a sus fondos.',
no_transactions: 'No hay transacciones todavía',
manage_extensions: 'Administrar extensiones',
manage_server: 'Administrar servidor',
extensions: 'Extensiones',
no_extensions: 'No tienes extensiones instaladas :(',
created: 'Creado',
search_extensions: 'Extensiones de búsqueda',
warning: 'Advertencia',
manage: 'Administrar',
repository: 'Repositorio',
confirm_continue: '¿Está seguro de que desea continuar?',
manage_extension_details: 'Instalar/desinstalar extensión',
install: 'Instalar',
uninstall: 'Desinstalar',
open: 'Abrir',
enable: 'Habilitar',
enable_extension_details: 'Habilitar extensión para el usuario actual',
disable: 'Deshabilitar',
installed: 'Instalado',
activated: 'Activado',
deactivated: 'Desactivado',
release_notes: 'Notas de la versión',
activate_extension_details:
'Hacer que la extensión esté disponible/no disponible para los usuarios',
featured: 'Destacado',
all: 'Todos',
only_admins_can_install:
'(Solo las cuentas de administrador pueden instalar extensiones)',
new_version: 'Nueva Versión',
extension_depends_on: 'Depende de:',
extension_rating_soon: 'Calificaciones próximamente',
extension_installed_version: 'Versión instalada',
extension_uninstall_warning:
'Está a punto de eliminar la extensión para todos los usuarios.',
uninstall_confirm: 'Sí, desinstalar',
extension_min_lnbits_version:
'Esta versión requiere al menos una versión de LNbits',
payment_hash: 'Hash de pago',
fee: 'Cuota',
amount: 'Cantidad',
+92
View File
@@ -0,0 +1,92 @@
window.localisation.fr = {
server: 'Serveur',
theme: 'Thème',
funding: 'Financement',
users: 'Utilisateurs',
restart: 'Redémarrer le serveur',
save: 'Enregistrer',
save_tooltip: 'Enregistrer vos modifications',
topup: 'Renflouer',
topup_wallet: 'Reflouer un portefeuille',
topup_hint:
"Utilisez l'ID du portefeuille pour recharger n'importe quel portefeuille",
restart_tooltip:
'Redémarrez le serveur pour que les changements prennent effet',
add_funds_tooltip: 'Ajouter des fonds à un portefeuille.',
reset_defaults: 'Réinitialiser aux valeurs par défaut',
reset_defaults_tooltip:
'Supprimer tous les paramètres et les réinitialiser aux valeurs par défaut.',
download_backup: 'Télécharger la sauvegarde de la base de données',
name_your_wallet: 'Nommez votre portefeuille %{name}',
paste_invoice_label:
'Coller une facture, une demande de paiement ou un code lnurl *',
lnbits_description:
"Facile à installer et léger, LNbits peut fonctionner sur n'importe quelle source de financement du réseau Lightning, prenant actuellement en charge LND, Core Lightning, OpenNode, LNPay et même LNbits lui-même! Vous pouvez exécuter LNbits pour vous-même ou offrir facilement une solution de gardien pour les autres. Chaque portefeuille a ses propres clés API et il n'y a pas de limite au nombre de portefeuilles que vous pouvez créer. La capacité de partitionner les fonds rend LNbits un outil utile pour la gestion de l'argent et comme outil de développement. Les extensions ajoutent une fonctionnalité supplémentaire à LNbits afin que vous puissiez expérimenter une gamme de technologies de pointe sur le réseau Lightning. Nous avons rendu le développement d'extensions aussi simple que possible et, en tant que projet gratuit et open source, nous encourageons les gens à développer et à soumettre les leurs.",
export_to_phone: 'Exporter vers le téléphone avec un code QR',
export_to_phone_desc:
"Ce code QR contient l'URL de votre portefeuille avec un accès complet. Vous pouvez le scanner depuis votre téléphone pour ouvrir votre portefeuille depuis là-bas.",
wallets: 'Portefeuilles',
add_wallet: 'Ajouter un nouveau portefeuille',
delete_wallet: 'Supprimer le portefeuille',
delete_wallet_desc:
'Ce portefeuille entier sera supprimé et les fonds seront IRRECUPERABLES.',
rename_wallet: 'Renommer le portefeuille',
update_name: 'Mettre à jour le nom',
press_to_claim: 'Appuyez pour demander du Bitcoin',
donate: 'Donner',
view_github: 'Voir sur GitHub',
voidwallet_active: 'VoidWallet est actif! Paiements désactivés',
use_with_caution:
'UTILISER AVEC PRUDENCE - Le portefeuille %{name} est toujours en version BETA',
toggle_darkmode: 'Basculer le mode sombre',
view_swagger_docs: "Voir les documents de l'API Swagger de LNbits",
api_docs: "Documents de l'API",
commit_version: 'Version de commit',
lnbits_version: 'Version de LNbits',
runs_on: 'Fonctionne sur',
credit_hint: 'Appuyez sur Entrée pour créditer le compte',
credit_label: '%{denomination} à créditer',
paste_request: 'Coller la requête',
create_invoice: 'Créer une facture',
camera_tooltip: 'Utiliser la caméra pour scanner une facture / un code QR',
export_csv: 'Exporter vers CSV',
transactions: 'Transactions',
chart_tooltip: 'Afficher le graphique',
pending: 'En attente',
copy_invoice: 'Copier la facture',
close: 'Fermer',
cancel: 'Annuler',
scan: 'Scanner',
read: 'Lire',
pay: 'Payer',
memo: 'Mémo',
date: 'Date',
processing_payment: 'Traitement du paiement...',
not_enough_funds: 'Fonds insuffisants !',
search_by_tag_memo_amount: 'Rechercher par tag, mémo, montant',
invoice_waiting: 'Facture en attente de paiement',
payment_received: 'Paiement reçu',
payment_sent: 'Paiement envoyé',
outgoing_payment_pending: 'Paiement sortant en attente',
drain_funds: 'Vider les fonds',
drain_funds_desc:
"Il s'agit d'un code QR LNURL-withdraw pour tout aspirer de ce portefeuille. Ne le partagez avec personne. Il est compatible avec balanceCheck et balanceNotify, de sorte que votre portefeuille peut continuer à retirer les fonds continuellement à partir d'ici après le premier retrait.",
i_understand: "J'ai compris",
copy_wallet_url: "Copier l'URL du portefeuille",
disclaimer_dialog:
"La fonctionnalité de connexion sera publiée dans une future mise à jour, pour l'instant, assurez-vous de mettre cette page en favori pour accéder à votre portefeuille ultérieurement ! Ce service est en BETA, et nous ne sommes pas responsables des personnes qui perdent l'accès à leurs fonds.",
no_transactions: 'Aucune transaction effectuée pour le moment',
manage_extensions: 'Gérer les extensions',
manage_server: 'Gérer le serveur',
extensions: 'Extensions',
no_extensions: "Vous n'avez installé aucune extension :(",
created: 'Créé',
payment_hash: 'Hash de paiement',
fee: 'Frais',
amount: 'Montant',
unit: 'Unité',
description: 'Description',
expiry: 'Expiration',
webhook: 'Webhook',
payment_proof: 'Preuve de paiement'
}
+89
View File
@@ -0,0 +1,89 @@
window.localisation.it = {
server: 'Server',
theme: 'Tema',
funding: 'Funding',
users: 'Utenti',
restart: 'Riavvia il server',
save: 'Salva',
save_tooltip: 'Salva le modifiche',
topup: 'Ricarica',
topup_wallet: 'Ricarica un portafoglio',
topup_hint: "Usa l'ID del portafoglio per ricaricare qualsiasi portafoglio",
restart_tooltip: 'Riavvia il server affinché le modifiche abbiano effetto',
add_funds_tooltip: 'Aggiungere fondi a un portafoglio',
reset_defaults: 'Ripristina le impostazioni predefinite',
reset_defaults_tooltip:
'Cancella tutte le impostazioni e ripristina i valori predefiniti',
download_backup: 'Scarica il backup del database',
name_your_wallet: 'Dai un nome al tuo portafoglio %{name}',
paste_invoice_label:
'Incolla una fattura, una richiesta di pagamento o un codice lnurl *',
lnbits_description:
"Leggero e facile da configurare, LNbits può funzionare su qualsiasi fonte di finanziamento lightning-network, attualmente supporta LND, Core Lightning, OpenNode, LNPay e persino LNbits stesso! Potete gestire LNbits per conto vostro o offrire facilmente una soluzione di custodia per altri. Ogni portafoglio ha le proprie chiavi API e non c'è limite al numero di portafogli che si possono creare. La possibilità di suddividere i fondi rende LNbits uno strumento utile per la gestione del denaro e come strumento di sviluppo. Le estensioni aggiungono ulteriori funzionalità a LNbits, consentendo di sperimentare una serie di tecnologie all'avanguardia sulla rete Lightning. Abbiamo reso lo sviluppo delle estensioni il più semplice possibile e, in quanto progetto libero e open-source, incoraggiamo le persone a sviluppare e inviare le proprie",
export_to_phone: 'Esportazione su telefono con codice QR',
export_to_phone_desc:
"Questo codice QR contiene l'URL del portafoglio con accesso da amministratore. È possibile scansionarlo dal telefono per aprire il portafoglio da lì.",
wallets: 'Portafogli',
add_wallet: 'Aggiungi un nuovo portafoglio',
delete_wallet: 'Elimina il portafoglio',
delete_wallet_desc:
"L'intero portafoglio sarà cancellato, i fondi saranno irrecuperabili",
rename_wallet: 'Rinomina il portafoglio',
update_name: 'Aggiorna il nome',
press_to_claim: 'Premi per richiedere bitcoin',
donate: 'Donazioni',
view_github: 'Visualizza su GitHub',
voidwallet_active: 'VoidWallet è attivo! Pagamenti disabilitati',
use_with_caution: 'USARE CON CAUTELA - %{nome} portafoglio è ancora in BETA',
toggle_darkmode: 'Attiva la modalità notturna',
view_swagger_docs: "Visualizza i documenti dell'API Swagger di LNbits",
api_docs: 'Documenti API',
commit_version: 'Commit version',
lnbits_version: 'Versione di LNbits',
runs_on: 'Esegue su',
credit_hint: 'Premere Invio per accreditare i fondi',
credit_label: '%{denomination} da accreditare',
paste_request: 'Richiesta di pagamento',
create_invoice: 'Crea fattura',
camera_tooltip: 'Usa la fotocamera per scansionare la fattura/QR',
export_csv: 'Esporta CSV',
transactions: 'Transazioni',
chart_tooltip: 'Mostra grafico',
pending: 'In attesa',
copy_invoice: 'Copia fattura',
close: 'Chiudi',
cancel: 'Annulla',
scan: 'Scansiona',
read: 'Leggi',
pay: 'Paga',
memo: 'Memo',
data: 'Dati',
processing_payment: 'Elaborazione pagamento...',
not_enough_funds: 'Non ci sono abbastanza fondi!',
search_by_tag_memo_amount: 'Cerca per tag, memo, importo...',
invoice_waiting: 'Fattura in attesa di pagamento',
payment_received: 'Pagamento ricevuto',
payment_sent: 'Pagamento inviato',
outgoing_payment_pending: 'Pagamento in uscita in attesa',
drain_funds: 'Fondi di drenaggio',
drain_funds_desc:
'Questo è un codice QR <code>LNURL-withdraw</code> per prelevare tutti i fondi da questo portafoglio. Non condividerlo con nessuno. È compatibile con <code>balanceCheck</code> e <code>balanceNotify</code>, di conseguenza il vostro portafoglio può continuare a prelevare continuamente i fondi da qui dopo il primo prelievo',
i_understand: 'Ho capito',
copy_wallet_url: 'Copia URL portafoglio',
disclaimer_dialog:
"La funzionalità di login sarà rilasciata in un futuro aggiornamento; per ora, assicuratevi di salvare tra i preferiti questa pagina per accedere nuovamente in futuro a questo portafoglio! Questo servizio è in fase BETA e non ci assumiamo alcuna responsabilità per la perdita all'accesso dei fondi",
no_transactions: 'Nessuna transazione effettuata',
manage_extensions: 'Gestisci le estensioni',
manage_server: 'Gestisci server',
estensioni: 'Estensioni',
no_extensions: 'Non ci sono estensioni installate :(',
created: 'Creato',
payment_hash: 'Hash del pagamento',
fee: 'Tariffa',
amount: 'Importo',
unit: 'Unità',
description: 'Descrizione',
expiry: 'Scadenza',
webhook: 'Webhook',
prova_di_pagamento: 'Prova di pagamento'
}
+32 -2
View File
@@ -18,7 +18,7 @@ window.localisation.jp = {
name_your_wallet: 'あなたのウォレットの名前 %{name}',
paste_invoice_label: '請求書を貼り付けてください',
lnbits_description:
'簡単にインストールでき、軽量で、LNbitsは現在LND、c-lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
export_to_phone: '電話にエクスポート',
export_to_phone_desc:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
@@ -70,11 +70,41 @@ window.localisation.jp = {
disclaimer_dialog:
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
no_transactions: 'トランザクションはありません',
manage_extensions: '拡張機能を管理する',
manage_server: 'サーバーを管理する',
extensions: '拡張機能',
no_extensions: '拡張機能はありません',
created: '作成済み',
search_extensions: '検索拡張機能',
warning: '警告',
manage: '管理',
repository: 'リポジトリ',
confirm_continue: '続行してもよろしいですか?',
manage_extension_details: '拡張機能のインストール/アンインストール',
install: 'インストール',
uninstall: 'アンインストール',
open: '開く',
enable: '有効',
enable_extension_details: '現在のユーザーの拡張機能を有効にする',
disable: '無効',
installed: 'インストール済み',
activated: '有効化',
deactivated: '無効化',
release_notes: 'リリースノート',
activate_extension_details:
'拡張機能をユーザーが利用できるようにする/利用できないようにする',
featured: '特集',
all: 'すべて',
only_admins_can_install:
'(管理者アカウントのみが拡張機能をインストールできます)',
new_version: '新しいバージョン',
extension_depends_on: '依存先:',
extension_rating_soon: '評価は近日公開',
extension_installed_version: 'インストール済みバージョン',
extension_uninstall_warning:
'すべてのユーザーの拡張機能を削除しようとしています.',
uninstall_confirm: 'はい、アンインストールします',
extension_min_lnbits_version:
'このリリースには少なくとも LNbits バージョンが必要です',
payment_hash: '支払いハッシュ',
fee: '料金',
amount: '量',
+90
View File
@@ -0,0 +1,90 @@
window.localisation.nl = {
server: 'Server',
theme: 'Thema',
funding: 'Financiering',
users: 'Gebruikers',
restart: 'Server opnieuw opstarten',
save: 'Opslaan',
save_tooltip: 'Sla uw wijzigingen op',
topup: 'Bijvullen',
topup_wallet: 'Een portemonnee bijvullen',
topup_hint: 'Gebruik de portemonnee-ID om elke portemonnee bij te vullen',
restart_tooltip:
'Start de server opnieuw op zodat wijzigingen van kracht worden',
add_funds_tooltip: 'Voeg geld toe aan een portemonnee.',
reset_defaults: 'Standaardinstellingen herstellen',
reset_defaults_tooltip:
'Wis alle instellingen en herstel de standaardinstellingen.',
download_backup: 'Databaseback-up downloaden',
name_your_wallet: 'Geef je %{name} portemonnee een naam',
paste_invoice_label: 'Plak een factuur, betalingsverzoek of lnurl-code*',
lnbits_description:
'Gemakkelijk in te stellen en lichtgewicht, LNbits kan op elke lightning-netwerkfinancieringsbron draaien, ondersteunt op dit moment LND, Core Lightning, OpenNode, LNPay en zelfs LNbits zelf! U kunt LNbits voor uzelf laten draaien of gemakkelijk een bewaardersoplossing voor anderen bieden. Elke portemonnee heeft zijn eigen API-sleutels en er is geen limiet aan het aantal portemonnees dat u kunt maken. Het kunnen partitioneren van fondsen maakt LNbits een nuttige tool voor geldbeheer en als ontwikkelingstool. Extensies voegen extra functionaliteit toe aan LNbits, zodat u kunt experimenteren met een reeks toonaangevende technologieën op het bliksemschichtnetwerk. We hebben het ontwikkelen van extensies zo eenvoudig mogelijk gemaakt en als een gratis en opensource-project moedigen we mensen aan om hun eigen ontwikkelingen in te dienen.',
export_to_phone: 'Exporteren naar telefoon met QR-code',
export_to_phone_desc:
'Deze QR-code bevat uw portemonnee-URL met volledige toegang. U kunt het vanaf uw telefoon scannen om uw portemonnee van daaruit te openen.',
wallets: 'Portemonnees',
add_wallet: 'Een nieuwe portemonnee toevoegen',
delete_wallet: 'Portemonnee verwijderen',
delete_wallet_desc:
'Deze hele portemonnee wordt verwijderd, de fondsen worden NIET TERUGGEVONDEN.',
rename_wallet: 'Portemonnee hernoemen',
update_name: 'Naam bijwerken',
press_to_claim: 'Druk om bitcoin te claimen',
donate: 'Doneren',
view_github: 'Bekijken op GitHub',
voidwallet_active: 'VoidWallet is actief! Betalingen uitgeschakeld',
use_with_caution:
'GEBRUIK MET VOORZICHTIGHEID - %{name} portemonnee is nog in BETA',
toggle_darkmode: 'Donkere modus aan/uit',
view_swagger_docs: 'Bekijk LNbits Swagger API-documentatie',
api_docs: 'API-documentatie',
commit_version: 'Commit-versie',
lnbits_version: 'LNbits-versie',
runs_on: 'Draait op',
credit_hint: 'Druk op Enter om de rekening te crediteren',
credit_label: '%{denomination} te crediteren',
paste_request: 'Verzoek plakken',
create_invoice: 'Factuur aanmaken',
camera_tooltip: 'Gebruik de camera om een factuur/QR-code te scannen',
export_csv: 'Exporteer naar CSV',
transactions: 'Transacties',
chart_tooltip: 'Toon grafiek',
pending: 'In behandeling',
copy_invoice: 'Kopieer factuur',
close: 'Sluiten',
cancel: 'Annuleren',
scan: 'Scannen',
read: 'Lezen',
pay: 'Betalen',
memo: 'Memo',
date: 'Datum',
processing_payment: 'Verwerking betaling...',
not_enough_funds: 'Onvoldoende saldo!',
search_by_tag_memo_amount: 'Zoeken op tag, memo, bedrag',
invoice_waiting: 'Factuur wachtend op betaling',
payment_received: 'Betaling ontvangen',
payment_sent: 'Betaling verzonden',
voutgoing_payment_pending: 'Uitgaande betaling in behandeling',
drain_funds: 'Geld opnemen',
drain_funds_desc:
'Dit is een LNURL-withdraw QR-code om alles uit deze portemonnee te halen. Deel deze code niet met anderen. Het is compatibel met balanceCheck en balanceNotify zodat jouw portemonnee continu geld kan blijven opnemen vanaf hier na de eerste opname.',
i_understand: 'Ik begrijp het',
copy_wallet_url: 'Kopieer portemonnee-URL',
disclaimer_dialog:
'Inlogfunctionaliteit wordt uitgebracht in een toekomstige update. Zorg er nu voor dat je deze pagina als favoriet markeert om in de toekomst toegang te krijgen tot je portemonnee! Deze service is in BETA en we zijn niet verantwoordelijk voor mensen die de toegang tot hun fondsen verliezen.',
no_transactions: 'Er zijn nog geen transacties gedaan',
manage_extensions: 'Beheer extensies',
manage_server: 'Beheer server',
extensions: 'Extensies',
no_extensions: 'Je hebt geen extensies geïnstalleerd :(',
created: 'Aangemaakt',
payment_hash: 'Betalings-hash',
fee: 'Kosten',
amount: 'Bedrag',
unit: 'Eenheid',
description: 'Beschrijving',
expiry: 'Vervaldatum',
webhook: 'Webhook',
payment_proof: 'Betalingsbewijs'
}
+91
View File
@@ -0,0 +1,91 @@
window.localisation.pi = {
server: 'Cap`n',
theme: 'Theme',
funding: 'Funding',
users: 'Buccaneers',
restart: 'Arr, restart Cap`n',
save: 'Bury Treasure',
save_tooltip: 'Bury yer changes, matey',
topup: 'Top up the Chest',
topup_wallet: 'Add more doubloons to the chest',
topup_hint: 'Use the chest ID to top up any chest',
restart_tooltip: 'Restart the Cap`n for changes to take effect, arr!',
add_funds_tooltip: 'Add doubloons to a chest and make it heavier',
reset_defaults: 'Reset to Davy Jones Locker',
reset_defaults_tooltip:
'Scuttle all settings and reset to Davy Jones Locker. Aye, start anew!',
download_backup: 'Download database booty',
name_your_wallet: 'Name yer %{name} treasure chest',
paste_invoice_label: 'Paste a booty, payment request or lnurl code, matey!',
lnbits_description:
'Arr, easy to set up and lightweight, LNbits can run on any lightning-network funding source, currently supporting LND, Core Lightning, OpenNode, LNPay and even LNbits itself! Ye can run LNbits for yourself, or easily offer a custodian solution for others. Each chest has its own API keys and there be no limit to the number of chests ye can make. Being able to partition booty makes LNbits a useful tool for money management and as a development tool. Arr, extensions add extra functionality to LNbits so ye can experiment with a range of cutting-edge technologies on the lightning network. We have made developing extensions as easy as possible, and as a free and open-source project, we encourage scallywags to develop and submit their own.',
export_to_phone: 'Export to Phone with QR Code, me hearties',
export_to_phone_desc:
'This QR code contains yer chest URL with full access. Ye can scan it from yer phone to open yer chest from there, arr!',
wallets: 'Treasure Chests',
add_wallet: 'Add a new chest and fill it with doubloons!',
delete_wallet: 'Scuttle the Chest',
delete_wallet_desc:
'This whole chest will be scuttled, the booty will be UNRECOVERABLE. Aye, be warned!',
rename_wallet: 'Rename the Chest, me hearty',
update_name: 'Update name like a captain',
press_to_claim: 'Press to claim gold doubloons, matey!',
donate: 'Donate like a true pirate!',
view_github: 'View on GitHub and find treasures',
voidwallet_active: 'VoidWallet be active! Payments disabled',
use_with_caution:
'USE WITH CAUTION - %{name} chest be still in BETA. Aye, be careful!',
toggle_darkmode: 'Toggle Dark Mode, arr!',
view_swagger_docs: 'View LNbits Swagger API docs and learn the secrets',
api_docs: 'API docs for the scallywags',
commit_version: 'Commit version like a true pirate',
lnbits_version: 'LNbits version, arr!',
runs_on: 'Runs on, matey',
credit_hint: 'Press Enter to credit account and make it richer',
credit_label: '%{denomination} to credit, arr!',
paste_request: 'Paste Request and find treasures',
create_invoice: 'Create Booty Request and get rich, me hearties!',
camera_tooltip: 'Use spyglass to scan a booty/QR, arr!',
export_csv: 'Export to CSV and keep track of the booty',
transactions: 'Pirate Transactions and loot',
chart_tooltip: 'Show ye chart, me hearty',
pending: 'Pendin like a ship at anchor',
copy_invoice: 'Copy booty request, arrr',
close: 'Batten down the hatches, we be closin',
cancel: 'Abandon ship! We be retreatin',
scan: 'Avast! Scan me beauty, arrr',
read: 'Read it, if ye dare',
pay: 'Pay up or walk the plank, ye scallywag',
memo: 'Message in a bottle, argh',
date: 'Date of the map, me matey',
processing_payment: 'Processing yer payment... don´t make me say it again',
not_enough_funds: 'Arrr, ye don´t have enough doubloons! Walk the plank!',
search_by_tag_memo_amount: 'Search by tag, message, or booty amount, savvy',
invoice_waiting: 'Invoice waiting to be plundered, arrr',
payment_received: 'Payment Received like a treasure, argh',
payment_sent: 'Payment Sent, hoist the colors! We´ve got some doubloons!',
outgoing_payment_pending:
'Outgoing payment pending in the port, ye scurvy dog',
drain_funds: 'Plunder all the doubloons, ye buccaneer',
drain_funds_desc:
'This be an LNURL-withdraw QR code for slurpin everything from this wallet. Don`t share with anyone. It be compatible with balanceCheck and balanceNotify so yer wallet may keep pullin` the funds continuously from here after the first withdraw.',
i_understand: 'I understand, yo ho ho and a bottle of rum!',
copy_wallet_url: 'Copy wallet URL like a map, savvy',
disclaimer_dialog:
'Login functionality to be released in a future update, for now, make sure ye bookmark this page for future access to your booty! This service be in BETA, and we hold no responsibility for people losing access to doubloons.',
no_transactions: 'No transactions made yet, me hearties. Belay that!',
manage_extensions: 'Manage Yer Extensions, ye landlubber',
manage_server: 'Manage Yer Server, me hearty',
extensions: 'Yer Extensions, ye scurvy dog',
no_extensions:
"Ye don't have any extensions installed, ye scallywag :(. Where be yer loot?",
created: 'Created like a legend, savvy',
payment_hash: 'Payment Hash like a treasure map, arrr',
fee: 'Fee like a toll to cross a strait, matey',
amount: 'Amount of doubloons, arrr',
unit: 'Unit of measurement like a fathom, ye buccaneer',
description: 'Description like a tale of adventure, arrr',
expiry: 'Expiry like the food on a ship, ye landlubber',
webhook: 'Webhook like a fishing line, arrr',
payment_proof: 'Payment Proof like a seal of authenticity, argh'
}
+87
View File
@@ -0,0 +1,87 @@
window.localisation.pl = {
server: 'Serwer',
theme: 'Motyw',
funding: 'Finansowanie',
users: 'Użytkownicy',
restart: 'Restart serwera',
save: 'Zapisz',
save_tooltip: 'Zapisz zmiany',
topup: 'Doładowanie',
topup_wallet: 'Doładuj portfel',
topup_hint: 'Użyj ID portfela aby go doładować',
restart_tooltip: 'Zrestartuj serwer aby aktywować zmiany',
add_funds_tooltip: 'Dodaj środki do portfela.',
reset_defaults: 'Powrót do ustawień domyślnych',
reset_defaults_tooltip: 'Wymaż wszystkie ustawienia i ustaw domyślne.',
download_backup: 'Pobierz kopię zapasową bazy danych',
name_your_wallet: 'Nazwij swój portfel %{name}',
paste_invoice_label: 'Wklej fakturę, żądanie zapłaty lub kod lnurl *',
lnbits_description:
'Łatwy i lekki w konfiguracji, LNbits może działać w oparciu o dowolne źródło finansowania w sieci lightning, obecnie wspiera LND, Core Lightning, OpenNode, LNPay czy nawet inną instancję LNbits! Możesz uruchomić instancję LNbits dla siebie lub dla innych. Każdy portfel ma swoje klucze API i nie ma ograniczeń jeśli chodzi o ilość portfeli. LNbits umożliwia dzielenie środków w celu zarządzania nimi, jest również dobrym narzędziem deweloperskim. Rozszerzenia zwiększają funkcjonalność LNbits co umożliwia eksperymentowanie z nowym technologiami w sieci lightning. Tworzenie rozszerzeń jest proste dlatego zachęcamy innych deweloperów do tworzenia dodatkowych funkcjonalności i wysyłanie do nas PR',
export_to_phone: 'Eksport kodu QR na telefon',
export_to_phone_desc:
'Ten kod QR zawiera adres URL Twojego portfela z pełnym dostępem do niego. Możesz go zeskanować na swoim telefonie aby otworzyć na nim ten portfel.',
wallets: 'Portfele',
add_wallet: 'Dodaj portfel',
delete_wallet: 'Usuń portfel',
delete_wallet_desc:
'Ten portfel zostanie usunięty, środków na nim zgromadzonych NIE BĘDZIE MOŻNA ODZYSKAĆ.',
rename_wallet: 'Zmień nazwę portfela',
update_name: 'Zaktualizuj nazwę',
press_to_claim: 'Naciśnij aby odebrać Bitcoiny',
donate: 'Podaruj',
view_github: 'Otwórz GitHub',
voidwallet_active: 'VoidWallet jest aktywny! Płatności są niemożliwe',
use_with_caution: 'KORZYSTAJ Z ROZWAGĄ - portfel %{name} jest w wersji BETA',
toggle_darkmode: 'Tryb nocny',
view_swagger_docs: 'Dokumentacja Swagger API',
api_docs: 'Dokumentacja API',
commit_version: 'Commit',
lnbits_version: 'Wersja LNbits',
runs_on: 'Działa na',
credit_hint: 'Naciśnij Enter aby doładować konto',
credit_label: '%{denomination} doładowanie',
paste_request: 'Wklej żądanie',
create_invoice: 'Utwórz fakturę',
camera_tooltip: 'Użyj kamery aby zeskanować fakturę lub kod QR',
export_csv: 'Eksport do CSV',
transactions: 'Transakcje',
chart_tooltip: 'Wykres',
pending: 'W toku',
copy_invoice: 'Skopiuj fakturę',
close: 'Zamknij',
cancel: 'Anuluj',
scan: 'Skanuj',
read: 'Odczytaj',
pay: 'Zapłać',
memo: 'Memo',
date: 'Data',
processing_payment: 'Przetwarzam płatność...',
not_enough_funds: 'Brak wystarczających środków!',
search_by_tag_memo_amount: 'Szukaj po tagu, memo czy wartości',
invoice_waiting: 'Faktura oczekuje na zapłatę',
payment_received: 'Otrzymano płatność',
payment_sent: 'Wysłano płatność',
outgoing_payment_pending: 'Płatność wychodząca w toku',
drain_funds: 'Opróżnij środki',
drain_funds_desc:
'To jest kod QR służący do opróżnienia portfela (LNURL-withdraw). Nie udostępniaj go nikomu. Ten kod jest kompatybilny z funkcjami, które umożliwiają wielokrotne żądania aż do zupełnego opróżnienia portfela.',
i_understand: 'Rozumiem',
copy_wallet_url: 'Skopiuj URL portfela',
disclaimer_dialog:
'Funkcja logowania zostanie uruchomiona w przyszłości. Póki co upewnij się, że zapisałeś adres URL tej strony aby mieć dostęp do tego portfela. Nie udostępniaj adresu tej strony nikomu, kto nie ma mieć do tego portfela dostępu! Ta usługa działa w wersji BETA, nie odpowiadamy za utratę dostępu do środków przez osoby używające LNbits.',
no_transactions: 'Brak transakcji',
manage_extensions: 'Zarządzaj rozszerzeniami',
manage_server: 'Zarządzaj serwerem',
extensions: 'Rozszerzenia',
no_extensions: 'Nie masz zainstalowanych żadnych rozszerzeń :(',
created: 'Utworzono',
payment_hash: 'Hash Płatności',
fee: 'Opłata',
amount: 'Wartość',
unit: 'Jednostka',
description: 'Opis',
expiry: 'Wygasa',
webhook: 'Webhook',
payment_proof: 'Potwierdzenie płatności'
}
+88
View File
@@ -0,0 +1,88 @@
window.localisation.pt = {
server: 'Servidor',
theme: 'Tema',
funding: 'Financiamento',
users: 'Usuários',
restart: 'Reiniciar servidor',
save: 'Gravar',
save_tooltip: 'Gravar as alterações',
topup: 'Reforçar conta',
topup_wallet: 'Recarregar uma carteira',
topup_hint: 'Use o ID da carteira para recarregar qualquer carteira',
restart_tooltip: 'Reinicie o servidor para que as alterações tenham efeito',
add_funds_tooltip: 'Adicionar fundos a uma carteira.',
reset_defaults: 'Redefinir para padrões',
reset_defaults_tooltip:
'Apagar todas as configurações e redefinir para os padrões.',
download_backup: 'Fazer backup da base de dados',
name_your_wallet: 'Nomeie sua carteira %{name}',
paste_invoice_label: 'Cole uma fatura, pedido de pagamento ou código lnurl *',
lnbits_description:
'Fácil de configurar e leve, o LNbits pode ser executado em qualquer fonte de financiamento da lightning-network, atualmente suportando LND, c-lightning, OpenNode, LNPay e até mesmo o LNbits em si! Você pode executar o LNbits para si mesmo ou oferecer facilmente uma solução de custódia para outros. Cada carteira tem suas próprias chaves de API e não há limite para o número de carteiras que você pode criar. Ser capaz de particionar fundos torna o LNbits uma ferramenta útil para gerenciamento de dinheiro e como uma ferramenta de desenvolvimento. As extensões adicionam funcionalidades extras ao LNbits para que você possa experimentar uma série de tecnologias de ponta na rede lightning. Nós tornamos o desenvolvimento de extensões o mais fácil possível e, como um projeto gratuito e de código aberto, incentivamos as pessoas a desenvolver e enviar as suas próprias.',
export_to_phone: 'Exportar para o telefone com código QR',
export_to_phone_desc:
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
wallets: 'Carteiras',
add_wallet: 'Adicionar nova carteira',
delete_wallet: 'Excluir carteira',
delete_wallet_desc:
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
rename_wallet: 'Renomear carteira',
update_name: 'Atualizar nome',
press_to_claim: 'Pressione para solicitar bitcoin',
donate: 'Doar',
view_github: 'Ver no GitHub',
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
use_with_caution: 'USE COM CAUTELA - a carteira %{name} ainda está em BETA',
toggle_darkmode: 'Alternar modo escuro',
view_swagger_docs: 'Ver a documentação da API do LNbits Swagger',
api_docs: 'Documentação da API',
commit_version: 'Versão de commit',
lnbits_version: 'Versão do LNbits',
runs_on: 'Executa em',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '%{denomination} para creditar',
paste_request: 'Colar Pedido',
create_invoice: 'Criar Fatura',
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
export_csv: 'Exportar para CSV',
transactions: 'Transações',
chart_tooltip: 'Mostrar gráfico',
pending: 'Pendente',
copy_invoice: 'Copiar fatura',
close: 'Fechar',
cancel: 'Cancelar',
scan: 'Escanear',
read: 'Ler',
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
processing_payment: 'Processando pagamento...',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
invoice_waiting: 'Fatura aguardando pagamento',
payment_received: 'Pagamento Recebido',
payment_sent: 'Pagamento Enviado',
outgoing_payment_pending: 'Pagamento de saída pendente',
drain_funds: 'Esvasiar carteira',
drain_funds_desc:
'Este é um código QR de saque LNURL para sacar tudo desta carteira. Não o partilhe com ninguém. É compatível com balanceCheck e balanceNotify para que a sua carteira possa continuar levantando os fundos continuamente daqui após o primeiro saque.',
i_understand: 'Eu entendo',
copy_wallet_url: 'Copiar URL da carteira',
disclaimer_dialog:
'Funcionalidade de login a ser lançada numa atualização futura, por enquanto, certifique-se que marca esta página para acesso futuro à sua carteira! Este serviço está em BETA, e não nos responsabilizamos por pessoas que perderem o acesso aos fundos.',
no_transactions: 'Ainda não foram feitas transações',
manage_extensions: 'Gerir extensões',
manage_server: 'Gerir servidor',
extensions: 'Extensões',
no_extensions: 'Não há nenhuma extensão instalada :(',
created: 'Criado',
payment_hash: 'Hash de pagamento',
fee: 'Taxa',
amount: 'Quantidade',
unit: 'Unidade',
description: 'Descrição',
expiry: 'Validade',
webhook: 'Webhook',
payment_proof: 'Comprovativo de pagamento'
}
+88
View File
@@ -0,0 +1,88 @@
window.localisation.we = {
server: 'Gweinydd',
theme: 'Thema',
funding: 'Arian fyndio',
users: 'Defnyddwyr',
restart: 'Ailgychwyn gweinydd',
save: 'Save',
save_tooltip: 'cadw eich newidiadau',
topup: 'Topup',
topup_wallet: 'Atodi waled',
topup_hint: 'Defnyddiwch ID y waled i ychwanegu at unrhyw waled',
restart_tooltip: 'Ailgychwyn y gweinydd er mwyn i newidiadau ddod i rym',
add_funds_tooltip: 'Ychwanegu arian at waled.',
reset_defaults: 'Ailosod i`r rhagosodiadau',
reset_defaults_tooltip: 'Dileu pob gosodiad ac ailosod i`r rhagosodiadau.',
download_backup: 'Lawrlwytho copi wrth gefn cronfa ddata',
name_your_wallet: 'Enwch eich waled %{name}',
paste_invoice_label: 'Gludwch anfoneb, cais am daliad neu god lnurl *',
lnbits_description:
'Yn hawdd iw sefydlu ac yn ysgafn, gall LNbits redeg ar unrhyw ffynhonnell ariannu rhwydwaith mellt, ar hyn o bryd yn cefnogi LND, Core Lightning, OpenNode, LNPay a hyd yn oed LNbits ei hun! Gallwch redeg LNbits i chi`ch hun, neu gynnig datrysiad ceidwad i eraill yn hawdd. Mae gan bob waled ei allweddi API ei hun ac nid oes cyfyngiad ar nifer y waledi y gallwch eu gwneud. Mae gallu rhannu cronfeydd yn gwneud LNbits yn arf defnyddiol ar gyfer rheoli arian ac fel offeryn datblygu. Mae estyniadau yn ychwanegu ymarferoldeb ychwanegol at LNbits fel y gallwch arbrofi gydag ystod o dechnolegau blaengar ar y rhwydwaith mellt. Rydym wedi gwneud datblygu estyniadau mor hawdd â phosibl, ac fel prosiect ffynhonnell agored am ddim, rydym yn annog pobl i ddatblygu a chyflwyno eu rhai eu hunain.',
export_to_phone: 'Allforio i Ffôn gyda chod QR',
export_to_phone_desc:
'Mae`r cod QR hwn yn cynnwys URL eich waled gyda mynediad llawn. Gallwch ei sganio o`ch ffôn i agor eich waled oddi yno.',
waledi: 'Waledi',
add_wallet: 'Ychwanegu waled newydd',
delete_wallet: 'Dileu waled',
delete_wallet_desc:
'Bydd y waled gyfan hon yn cael ei dileu, ni fydd modd adennill yr arian.',
rename_wallet: 'Ailenwi waled',
update_name: 'Diweddaru enw',
press_to_claim: 'Pwyswch i hawlio bitcoin',
Donate: 'Rhoi',
view_github: 'Gweld ar GitHub',
voidwallet_active:
' Mae VoidWallet yn weithredol! Taliadau wedi`u hanalluogi',
use_with_caution: 'DEFNYDDIO GYDA GOFAL - mae waled %{name} yn dal yn BETA',
toggle_darkmode: 'Toglo Modd Tywyll',
view_swagger_docs: 'Gweld dogfennau API LNbits Swagger',
api_docs: 'Api docs',
commit_version: 'fersiwn ymrwymo',
lnbits_version: 'Fersiwn LNbits',
Runs_on: 'Yn rhedeg ymlaen',
credit_hint: 'Pwyswch Enter i gyfrif credyd',
credit_label: '%{enomination} i gredyd',
paste_request: 'Gludo Cais',
create_invoice: 'Creu Anfoneb',
camera_tooltip: 'Defnyddio camera i sganio anfoneb/QR',
export_csv: 'Allforio i CSV',
trafodion: 'Trafodion',
chart_tooltip: 'Dangos siart',
pending: 'yn yr arfaeth',
copy_invoice: 'Copi anfoneb',
Close: 'cau',
cancel: 'Canslo',
scan: 'Sgan',
read: 'Darllen',
talu: 'Pay',
memo: 'Memo',
date: 'Dyddiad',
processing_payment: 'Prosesu taliad...',
not_enough_funds: 'Dim digon o arian!',
search_by_tag_memo_amount: 'Chwilio yn ôl tag, memo, swm',
invoice_waiting: 'Anfoneb yn aros i gael ei thalu',
payment_received: 'Taliad a Dderbyniwyd',
payment_sent: 'Taliad a Anfonwyd',
outgoing_payment_pending: 'Taliad sy`n aros yn yr arfaeth',
drain_funds: 'Cronfeydd Draenio',
drain_funds_desc:
'Cod QR Tynnu`n ôl LNURL yw hwn ar gyfer slurpio popeth o`r waled hon. Peidiwch â rhannu gyda neb. Mae`n gydnaws â balanceCheck a balanceNotify felly efallai y bydd eich waled yn tynnu`r arian yn barhaus o`r fan hon ar ôl y codiad cyntaf.',
i_understand: 'Rwy`n deall',
copy_wallet_url: 'Copi URL waled',
disclaimer_dialog:
'Swyddogaeth mewngofnodi i`w ryddhau mewn diweddariad yn y dyfodol, am y tro, gwnewch yn siŵr eich bod yn rhoi nod tudalen ar y dudalen hon ar gyfer mynediad i`ch waled yn y dyfodol! Mae`r gwasanaeth hwn yn BETA, ac nid ydym yn gyfrifol am bobl sy`n colli mynediad at arian.',
no_transactions: 'Dim trafodion wedi`u gwneud eto',
manage_extensions: 'Rheoli Estyniadau',
manage_server: 'Rheoli Gweinydd',
Extensions: 'Estyniadau',
no_extensions: "Nid oes gennych unrhyw estyniadau wedi'u gosod :(",
create: 'Crëwyd',
payment_hash: 'Hais Taliad',
fee: 'Fee',
amount: 'swm',
unit: 'Uned',
description: 'Disgrifiad',
expiry: 'dod i ben',
webhook: 'bachyn we',
payment_proof: 'prawf taliad'
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+12 -4
View File
@@ -67,8 +67,13 @@ window.LNbits = {
getWallet: function (wallet) {
return this.request('get', '/api/v1/wallet', wallet.inkey)
},
getPayments: function (wallet) {
return this.request('get', '/api/v1/payments', wallet.inkey)
getPayments: function (wallet, query) {
const params = new URLSearchParams(query)
return this.request(
'get',
'/api/v1/payments/paginated?' + params,
wallet.inkey
)
},
getPayment: function (wallet, paymentHash) {
return this.request(
@@ -185,7 +190,7 @@ window.LNbits = {
},
payment: function (data) {
obj = {
checking_id: data.id,
checking_id: data.checking_id,
pending: data.pending,
amount: data.amount,
fee: data.fee,
@@ -338,7 +343,8 @@ window.windowMixin = {
user: null,
wallet: null,
payments: [],
allowedThemes: null
allowedThemes: null,
langs: []
}
}
},
@@ -383,6 +389,8 @@ window.windowMixin = {
window.i18n.locale = locale
}
this.g.langs = window.langs ?? []
addEventListener('offline', event => {
this.g.offline = true
})
+67 -73
View File
@@ -140,14 +140,6 @@ Vue.component('lnbits-extension-list', {
<q-item-label lines="1" class="text-caption" v-text="$t('extensions')"></q-item-label>
</q-item-section>
</q-item>
<q-item clickable tag="a" :href="['/install?usr=', user.id].join('')">
<q-item-section side>
<q-icon name="playlist_add" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption" v-text="$t('manage_extensions')"></q-item-label>
</q-item-section>
</q-item>
</q-list>
`,
computed: {
@@ -221,71 +213,73 @@ Vue.component('lnbits-payment-details', {
}
},
template: `
<div class="q-py-md" style="text-align: left">
<div class="row justify-center q-mb-md">
<q-badge v-if="hasTag" color="yellow" text-color="black">
#{{ payment.tag }}
</q-badge>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('created')"></b>:</div>
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('expiry')"></b>:</div>
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('description')"></b>:</div>
<div class="col-9">{{ payment.memo }}</div>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('amount')"></b>:</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('fee')"></b>:</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
<div class="col-9 text-wrap mono">
{{ payment.payment_hash }}
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
</div>
</div>
<div class="row" v-if="payment.webhook">
<div class="col-3"><b v-text="$t('webhook')"></b>:</div>
<div class="col-9 text-wrap mono">
{{ payment.webhook }}
<q-badge :color="webhookStatusColor" text-color="white">
{{ webhookStatusText }}
</q-badge>
</div>
</div>
<div class="row" v-if="hasPreimage">
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
<div class="col-9 text-wrap mono">{{ payment.preimage }}</div>
</div>
<div class="row" v-for="entry in extras">
<div class="col-3">
<q-badge v-if="hasTag" color="secondary" text-color="white">
extra
</q-badge>
<b>{{ entry.key }}</b>:
</div>
<div class="col-9 text-wrap mono">{{ entry.value }}</div>
</div>
<div class="row" v-if="hasSuccessAction">
<div class="col-3"><b>Success action</b>:</div>
<div class="col-9">
<lnbits-lnurlpay-success-action
:payment="payment"
:success_action="payment.extra.success_action"
></lnbits-lnurlpay-success-action>
</div>
</div>
</div>
<div class="q-py-md" style="text-align: left">
<div v-if="payment.tag" class="row justify-center q-mb-md">
<q-badge v-if="hasTag" color="yellow" text-color="black">
#{{ payment.tag }}
</q-badge>
</div>
<div class="row">
<b v-text="$t('created')"></b>:
{{ payment.date }} ({{ payment.dateFrom }})
</div>
<div class="row">
<b v-text="$t('expiry')"></b>:
{{ payment.expirydate }} ({{ payment.expirydateFrom }})
</div>
<div class="row">
<b v-text="$t('amount')"></b>:
{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
</div>
<div class="row">
<b v-text="$t('fee')"></b>:
{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
</div>
<div class="text-wrap">
<b style="white-space: nowrap;" v-text="$t('payment_hash')"></b>:&nbsp;{{ payment.payment_hash }}
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
</div>
<div class="text-wrap">
<b style="white-space: nowrap;" v-text="$t('memo')"></b>:&nbsp;{{ payment.memo }}
</div>
<div class="row" v-if="payment.webhook">
<b v-text="$t('webhook')"></b>:
{{ payment.webhook }}
<q-badge :color="webhookStatusColor" text-color="white">
{{ webhookStatusText }}
</q-badge>
</div>
<div class="row" v-if="hasPreimage">
<b v-text="$t('payment_proof')"></b>:
{{ payment.preimage }}
</div>
<div class="row" v-for="entry in extras">
<q-badge v-if="hasTag" color="secondary" text-color="white">
extra
</q-badge>
<b>{{ entry.key }}</b>:
{{ entry.value }}
</div>
<div class="row" v-if="hasSuccessAction">
<b>Success action</b>:
<lnbits-lnurlpay-success-action
:payment="payment"
:success_action="payment.extra.success_action"
></lnbits-lnurlpay-success-action>
</div>
</div>
`,
computed: {
hasPreimage() {
+1 -1
View File
@@ -188,7 +188,7 @@ video {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(../fonts/material-icons-v50.woff2) format('woff2');
src: url(/static/fonts/material-icons-v50.woff2) format('woff2');
}
.material-icons {
+39
View File
@@ -0,0 +1,39 @@
{
"js": [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/VueQrcodeReader.umd.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
"/static/vendor/vue-i18n.js",
"/static/i18n/i18n.js",
"/static/i18n/de.js",
"/static/i18n/en.js",
"/static/i18n/es.js",
"/static/i18n/fr.js",
"/static/i18n/it.js",
"/static/i18n/jp.js",
"/static/i18n/nl.js",
"/static/i18n/pi.js",
"/static/i18n/pl.js",
"/static/i18n/fr.js",
"/static/i18n/nl.js",
"/static/i18n/we.js",
"/static/i18n/pt.js",
"/static/i18n/br.js",
"/static/js/base.js",
"/static/js/components.js",
"/static/js/bolt11-decoder.js"
],
"css": [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/css/base.css"
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.wrapper[data-v-1f90552a]{position:relative;z-index:0;width:100%;height:100%}.overlay[data-v-1f90552a],.tracking-layer[data-v-1f90552a]{position:absolute;width:100%;height:100%;top:0;left:0}.camera[data-v-1f90552a],.pause-frame[data-v-1f90552a]{display:block;object-fit:cover;width:100%;height:100%}
+50 -41
View File
@@ -2,12 +2,9 @@
<html lang="en">
<head>
{% for url in VENDORED_CSS %}
{% for url in INCLUDED_CSS %}
<link rel="stylesheet" type="text/css" href="{{ url }}" />
{% endfor %}
<!---->
<link rel="stylesheet" type="text/css" href="/static/css/base.css" />
{% block styles %}{% endblock %}
{% endfor %} {% block styles %}{% endblock %}
<title>{% block title %}{{ SITE_TITLE }}{% endblock %}</title>
<meta charset="utf-8" />
<meta
@@ -43,15 +40,24 @@
<span><strong>LN</strong>bits</span> {% endif %} {%endif%} {%
endblock %}
</q-btn>
{% block toolbar_subtitle %}{%if user and user.super_user%}
<q-badge align="middle">Super User</q-badge>
{% elif user and user.admin %}
<q-badge align="middle">Admin User</q-badge>
{%endif%}{% endblock %}
</q-toolbar-title>
{% block beta %} {% if VOIDWALLET %}
<q-badge color="red" text-color="black" class="q-mr-md">
{% raw %}{{ $t('voidwallet_active') }}{% endraw %}
</q-badge>
{%endif%}
<q-badge color="yellow" text-color="black" class="q-mr-md">
<q-badge
v-show="$q.screen.gt.sm"
color="yellow"
text-color="black"
class="q-mr-md"
>
<span
v-show="$q.screen.gt.sm"
v-text='$t("use_with_caution", { name: "{{ SITE_TITLE }}" })'
></span>
</q-badge>
@@ -72,25 +78,18 @@
icon="language"
class="q-pl-md"
>
<q-list>
<q-item clickable v-close-popup @click="changeLanguage('en')">
<q-item-section>
<q-item-label>EN</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('de')">
<q-item-section>
<q-item-label>DE</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('es')">
<q-item-section>
<q-item-label>ES</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="changeLanguage('jp')">
<q-item-section>
<q-item-label>JP</q-item-label>
<q-list v-for="(lang, index) in g.langs" :key="index">
<q-item
clickable
v-close-popup
@click="changeLanguage(lang.value)"
><q-item-section>
{% raw %}
<q-item-label
>{{lang.display ?? lang.value.toUpperCase()}}</q-item-label
>
<q-tooltip>{{lang.label}}</q-tooltip>
{% endraw %}
</q-item-section>
</q-item>
</q-list>
@@ -239,14 +238,18 @@
class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}"
>
<q-space class="q-py-lg lt-sm"></q-space>
<q-toolbar class="gt-sm">
<q-toolbar-title class="text-caption">
{{ SITE_TITLE }}, {{SITE_TAGLINE}}
<br />
<small v-if="'{{SITE_TITLE}}' == 'LNbits'"
<small
>{% raw %}{{ $t('lnbits_version') }}{% endraw %}:
{{LNBITS_VERSION}},
</small>
<small
>{% raw %}{{ $t('commit_version') }}{% endraw %}:
{{LNBITS_VERSION}}</small
{{COMMIT_VERSION}}</small
>
</q-toolbar-title>
<q-space></q-space>
@@ -284,24 +287,30 @@
{% block vue_templates %}{% endblock %}
<!---->
{% for url in VENDORED_JS %}
{% for url in INCLUDED_JS %}
<script src="{{ url }}"></script>
{% endfor %}
<!---->
<script src="/static/i18n/i18n.js"></script>
<script src="/static/i18n/en.js"></script>
<script src="/static/i18n/de.js"></script>
<script src="/static/i18n/es.js"></script>
<script src="/static/i18n/jp.js"></script>
<script src="/static/js/base.js"></script>
<script src="/static/js/components.js"></script>
<script src="/static/js/bolt11-decoder.js"></script>
<script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
if(themes && themes.length) {
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson }}
if (themes && themes.length) {
window.allowedThemes = themes.map(str => str.trim())
}
window.langs = [
{value: 'en', label: 'English', display: 'EN'},
{value: 'de', label: 'Deutsch', display: 'DE'},
{value: 'es', label: 'Español', display: 'ES'},
{value: 'jp', label: '日本語', display: 'JP'},
{value: 'fr', label: 'Français', display: 'FR'},
{value: 'it', label: 'Italiano', display: 'IT'},
{value: 'pi', label: 'Pirate', display: 'PI'},
{value: 'nl', label: 'Nederlands', display: 'NL'},
{value: 'we', label: 'Cymraeg', display: 'CY'},
{value: 'pl', label: 'Polski', display: 'PL'},
{value: 'pt', label: 'Português', display: 'PT'},
{value: 'br', label: 'Português', display: 'BR'}
]
</script>
{% block scripts %}{% endblock %}
</body>
+2 -2
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
{% for url in VENDORED_CSS %}
{% for url in INCLUDED_CSS %}
<link rel="stylesheet" type="text/css" href="{{ url }}" />
{% endfor %}
<style>
@@ -33,7 +33,7 @@
</q-page-container>
</q-layout>
{% for url in VENDORED_JS %}
{% for url in INCLUDED_JS %}
<script src="{{ url }}"></script>
{% endfor %}
<!---->
+2 -2
View File
@@ -69,7 +69,7 @@ class CoreLightningWallet(Wallet):
try:
funds = self.ln.listfunds()
return StatusResponse(
None, sum([ch["channel_sat"] * 1000 for ch in funds["channels"]])
None, sum([int(ch["our_amount_msat"]) for ch in funds["channels"]])
)
except RpcError as exc:
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
@@ -141,7 +141,7 @@ class CoreLightningWallet(Wallet):
except Exception as exc:
return PaymentResponse(False, None, None, None, str(exc))
fee_msat = -int(r["msatoshi_sent"] - r["msatoshi"])
fee_msat = -int(r["amount_sent_msat"] - r["amount_msat"])
return PaymentResponse(
True, r["payment_hash"], fee_msat, r["payment_preimage"], None
)
-1
View File
@@ -104,4 +104,3 @@ in
};
};
}
+703 -40
View File
@@ -14,13 +14,16 @@
"underscore": "^1.13.6",
"vue": "2.6.12",
"vue-i18n": "^8.28.2",
"vue-qrcode-reader": "2.2",
"vue-qrcode-reader": "^2.3.18",
"vue-router": "3.4.3",
"vuex": "3.5.1"
},
"devDependencies": {
"concat": "^1.0.3",
"minify": "^9.2.0",
"prettier": "2.8.3",
"pyright": "1.1.289"
"pyright": "1.1.289",
"sass": "^1.60.0"
}
},
"node_modules/@chenfengyuan/vue-qrcode": {
@@ -35,6 +38,76 @@
"vue": "^2.6.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -71,6 +144,19 @@
"node": ">=7.0.0"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -86,22 +172,49 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
"license": "MIT",
"dependencies": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/callforth": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/callforth/-/callforth-0.3.1.tgz",
"integrity": "sha512-Q2zPfqnwoKsb1DTVCr4lmhe49wKNBsMmNlbudjleu3/co+Nw1pOqFHYJHrW3VZ253ou9AAr+xauQR0C55NPdzA==",
"license": "MIT"
},
"node_modules/camel-case": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
"dev": true,
"dependencies": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -140,6 +253,45 @@
"color-name": "^1.0.0"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
"integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==",
"dev": true,
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -183,12 +335,65 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/concat": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/concat/-/concat-1.0.3.tgz",
"integrity": "sha512-f/ZaH1aLe64qHgTILdldbvyfGiGF4uzeo9IuXUloIOLQzFmIPloy9QbZadNsuVv0j5qbKQvQb/H/UYf2UsKTpw==",
"dev": true,
"dependencies": {
"commander": "^2.9.0"
},
"bin": {
"concat": "bin/concat"
},
"engines": {
"node": ">=6"
}
},
"node_modules/core-js": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
"version": "3.30.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz",
"integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==",
"hasInstallScript": true,
"license": "MIT"
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-b64-images": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
"integrity": "sha512-TgQBEdP07adhrDfXvI5o6bHGukKBNMzp2Ngckc/6d09zpjD2gc1Hl3Ca1CKgb8FXjHi88+Phv2Uegs2kTL4zjg==",
"dev": true,
"bin": {
"css-b64-images": "bin/css-b64-images"
},
"engines": {
"node": "*"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
@@ -213,6 +418,16 @@
"integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==",
"license": "MIT"
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"dev": true,
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -225,6 +440,30 @@
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
"dev": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"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==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -271,6 +510,20 @@
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -280,6 +533,87 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/html-minifier-terser": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.1.0.tgz",
"integrity": "sha512-BvPO2S7Ip0Q5qt+Y8j/27Vclj6uHC6av0TMoDn7/bJPhMWHI2UtR2e/zEgJn3/qYAmxumrGp9q4UHurL6mtW9Q==",
"dev": true,
"dependencies": {
"camel-case": "^4.1.2",
"clean-css": "5.2.0",
"commander": "^9.4.1",
"entities": "^4.4.0",
"param-case": "^3.0.4",
"relateurl": "^0.2.7",
"terser": "^5.15.1"
},
"bin": {
"html-minifier-terser": "cli.js"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/html-minifier-terser/node_modules/clean-css": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz",
"integrity": "sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==",
"dev": true,
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/html-minifier-terser/node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -289,11 +623,32 @@
"node": ">=8"
}
},
"node_modules/jsqr": {
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jju": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
"integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
"dev": true
},
"node_modules/locate-path": {
"version": "5.0.0",
@@ -307,6 +662,15 @@
"node": ">=8"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dev": true,
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -326,6 +690,100 @@
"node": ">= 0.6"
}
},
"node_modules/minify": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/minify/-/minify-9.2.0.tgz",
"integrity": "sha512-dfVx8j27TIZy2EE/nVMAhTVmUo6tdO4gYbKXaCYPUmpqWN2QJBgQtPfofsoA658L3Ee7e5OcyFIncJRgFtZcqQ==",
"dev": true,
"dependencies": {
"clean-css": "^5.0.1",
"css-b64-images": "~0.2.5",
"debug": "^4.1.0",
"find-up": "^6.1.0",
"html-minifier-terser": "^7.1.0",
"readjson": "^2.2.2",
"simport": "^1.2.0",
"terser": "^5.3.2",
"try-catch": "^3.0.0",
"try-to-catch": "^3.0.0"
},
"bin": {
"minify": "bin/minify.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/minify/node_modules/find-up": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
"integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
"dev": true,
"dependencies": {
"locate-path": "^7.1.0",
"path-exists": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minify/node_modules/locate-path": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
"integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
"dev": true,
"dependencies": {
"p-locate": "^6.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minify/node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"dev": true,
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minify/node_modules/p-locate": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"dev": true,
"dependencies": {
"p-limit": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minify/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
@@ -334,6 +792,31 @@
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dev": true,
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -370,6 +853,26 @@
"node": ">=6"
}
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
"dev": true,
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
"dev": true,
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -379,6 +882,18 @@
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -455,11 +970,39 @@
"url": "https://donate.quasar.dev"
}
},
"node_modules/regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
"license": "MIT"
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/readjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/readjson/-/readjson-2.2.2.tgz",
"integrity": "sha512-PdeC9tsmLWBiL8vMhJvocq+OezQ3HhsH2HrN7YkhfYcTjQSa/iraB15A7Qvt7Xpr0Yd2rDNt6GbFwVQDg3HcAw==",
"dev": true,
"dependencies": {
"jju": "^1.4.0",
"try-catch": "^3.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
"dev": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
@@ -480,7 +1023,6 @@
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^2.6.0"
},
@@ -489,11 +1031,27 @@
"npm": ">=3.10.0"
}
},
"node_modules/sass": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.60.0.tgz",
"integrity": "sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw==",
"license": "MIT"
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
},
"node_modules/set-blocking": {
"version": "2.0.0",
@@ -501,6 +1059,47 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/simport": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/simport/-/simport-1.2.0.tgz",
"integrity": "sha512-85Bm7pKsqiiQ8rmYCaPDdlXZjJvuW6/k/FY8MTtLFMgU7f8S00CgTHfRtWB6KwSb6ek4p9YyG2enG1+yJbl+CA==",
"dev": true,
"dependencies": {
"readjson": "^2.2.0",
"try-to-catch": "^3.0.0"
},
"engines": {
"node": ">=12.2"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -527,6 +1126,60 @@
"node": ">=8"
}
},
"node_modules/terser": {
"version": "5.16.8",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz",
"integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/try-catch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz",
"integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/try-to-catch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz",
"integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
"dev": true
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
@@ -543,15 +1196,14 @@
"integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA=="
},
"node_modules/vue-qrcode-reader": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-2.2.0.tgz",
"integrity": "sha512-xsNptrJGTFFjwfksIK6KlbjsyIGBLTFgO9KH3JcmZzABY19c2d0+zG//LXkp8K2Jl+4APidktVtH/aTbmcgX7g==",
"license": "MIT",
"version": "2.3.18",
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-2.3.18.tgz",
"integrity": "sha512-r+nrIZQ+0helU3nfioh84iZDw39nh0w8NjMtOnWcM/3pqr9MOyUV1dkZWD144N1LB3/z5gUOAWkWHJ+6lq6rUA==",
"dependencies": {
"babel-runtime": "^6.26.0",
"callforth": "^0.3.0",
"jsqr": "^1.2.0",
"webrtc-adapter": "^6.2.1"
"callforth": "^0.3.1",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"webrtc-adapter": "7.7.0"
}
},
"node_modules/vue-router": {
@@ -568,13 +1220,12 @@
}
},
"node_modules/webrtc-adapter": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-6.4.8.tgz",
"integrity": "sha512-YM8yl545c/JhYcjGHgaCoA7jRK/KZuMwEDFeP2AcP0Auv5awEd+gZE0hXy9z7Ed3p9HvAXp8jdbe+4ESb1zxAw==",
"license": "BSD-3-Clause",
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.0.tgz",
"integrity": "sha512-7Bp9OBnx642oJRkom1tNAbeJjUadAq2rh5xLL9YXPw5hVyt2h4hHr5bcoPYDs1stp/mZHSPSQA34YISdnr0DBQ==",
"dependencies": {
"rtcpeerconnection-shim": "^1.2.14",
"sdp": "^2.9.0"
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
},
"engines": {
"node": ">=6.0.0",
@@ -641,6 +1292,18 @@
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"dev": true,
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+69 -2
View File
@@ -1,8 +1,20 @@
{
"name": "lnbits",
"scripts": {
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
"vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"",
"vendor_bundle_css": "node -e \"require('concat')(require('./package.json').bundle.css.map(a => './lnbits/'+a), './lnbits/static/bundle.css')\"",
"vendor_bundle_js": "node -e \"require('concat')(require('./package.json').bundle.js.map(a => './lnbits/'+a), './lnbits/static/bundle.js')\"",
"vendor_minify_css": "./node_modules/.bin/minify ./lnbits/static/bundle.css > ./lnbits/static/bundle.min.css",
"vendor_minify_js": "./node_modules/.bin/minify ./lnbits/static/bundle.js > ./lnbits/static/bundle.min.js"
},
"devDependencies": {
"concat": "^1.0.3",
"minify": "^9.2.0",
"prettier": "2.8.3",
"pyright": "1.1.289"
"pyright": "1.1.289",
"sass": "^1.60.0"
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "1.0.2",
@@ -13,8 +25,63 @@
"underscore": "^1.13.6",
"vue": "2.6.12",
"vue-i18n": "^8.28.2",
"vue-qrcode-reader": "2.2",
"vue-qrcode-reader": "^2.3.18",
"vue-router": "3.4.3",
"vuex": "3.5.1"
},
"vendor": [
"./node_modules/moment/moment.js",
"./node_modules/underscore/underscore.js",
"./node_modules/axios/dist/axios.js",
"./node_modules/vue/dist/vue.js",
"./node_modules/vue-router/dist/vue-router.js",
"./node_modules/vue-qrcode-reader/dist/VueQrcodeReader.umd.js",
"./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js",
"./node_modules/vuex/dist/vuex.js",
"./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js",
"./node_modules/quasar/dist/quasar.umd.js",
"./node_modules/chart.js/dist/Chart.bundle.js",
"./node_modules/quasar/dist/quasar.css",
"./node_modules/chart.js/dist/Chart.css",
"./node_modules/vue-i18n/dist/vue-i18n.js"
],
"bundle": {
"js": [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/VueQrcodeReader.umd.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
"/static/vendor/vue-i18n.js",
"/static/i18n/i18n.js",
"/static/i18n/de.js",
"/static/i18n/en.js",
"/static/i18n/es.js",
"/static/i18n/fr.js",
"/static/i18n/it.js",
"/static/i18n/jp.js",
"/static/i18n/nl.js",
"/static/i18n/pi.js",
"/static/i18n/pl.js",
"/static/i18n/fr.js",
"/static/i18n/nl.js",
"/static/i18n/we.js",
"/static/i18n/pt.js",
"/static/i18n/br.js",
"/static/js/base.js",
"/static/js/components.js",
"/static/js/bolt11-decoder.js"
],
"css": [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/css/base.css"
]
}
}
Generated
+178 -68
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "anyio"
@@ -35,14 +35,14 @@ files = [
[[package]]
name = "astroid"
version = "2.15.2"
version = "2.15.3"
description = "An abstract syntax tree for Python with inference support."
category = "dev"
optional = false
python-versions = ">=3.7.2"
files = [
{file = "astroid-2.15.2-py3-none-any.whl", hash = "sha256:dea89d9f99f491c66ac9c04ebddf91e4acf8bd711722175fe6245c0725cc19bb"},
{file = "astroid-2.15.2.tar.gz", hash = "sha256:6e61b85c891ec53b07471aec5878f4ac6446a41e590ede0f2ce095f39f7d49dd"},
{file = "astroid-2.15.3-py3-none-any.whl", hash = "sha256:f11e74658da0f2a14a8d19776a8647900870a63de71db83713a8e77a6af52662"},
{file = "astroid-2.15.3.tar.gz", hash = "sha256:44224ad27c54d770233751315fa7f74c46fa3ee0fab7beef1065f99f09897efe"},
]
[package.dependencies]
@@ -160,24 +160,6 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "boltz-client"
version = "0.1.3"
description = "python boltz client"
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "boltz_client-0.1.3-py3-none-any.whl", hash = "sha256:67a231de6cc0876376e32aa177eb572ef14e869a645ef4565f307daa527f1e76"},
{file = "boltz_client-0.1.3.tar.gz", hash = "sha256:1573be84ea547578591d78f5f1ea65ccda01edcca0aa6f077bcd5a497622cef5"},
]
[package.dependencies]
click = ">=8"
embit = ">=0.4"
httpx = ">=0.23"
websockets = ">=10"
[[package]]
name = "cashu"
version = "0.9.0"
@@ -343,6 +325,18 @@ files = [
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
[[package]]
name = "charset-normalizer"
version = "2.0.12"
@@ -556,6 +550,18 @@ files = [
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
[[package]]
name = "ecdsa"
version = "0.18.0"
@@ -586,19 +592,6 @@ files = [
{file = "embit-0.4.9.tar.gz", hash = "sha256:992332bd89af6e2d027e26fe437eb14aa33997db08c882c49064d49c3e6f4ab9"},
]
[[package]]
name = "enum34"
version = "1.1.10"
description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "enum34-1.1.10-py2-none-any.whl", hash = "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53"},
{file = "enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328"},
{file = "enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"},
]
[[package]]
name = "environs"
version = "9.5.0"
@@ -658,6 +651,22 @@ dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt]
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"]
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
[[package]]
name = "filelock"
version = "3.12.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
{file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
]
[package.extras]
docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
name = "flake8"
version = "6.0.0"
@@ -791,6 +800,21 @@ cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.22"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
{file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.4"
@@ -1117,6 +1141,21 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.7.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
[package.dependencies]
setuptools = "*"
[[package]]
name = "outcome"
version = "1.2.0"
@@ -1144,21 +1183,6 @@ files = [
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
]
[[package]]
name = "pathlib2"
version = "2.3.7.post1"
description = "Object-oriented filesystem paths"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"},
{file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"},
]
[package.dependencies]
six = "*"
[[package]]
name = "pathspec"
version = "0.11.1"
@@ -1203,6 +1227,25 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.2.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.2.2-py2.py3-none-any.whl", hash = "sha256:0b4210aea813fe81144e87c5a291f09ea66f199f367fa1df41b55e1d26e1e2b4"},
{file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "protobuf"
version = "4.21.12"
@@ -1490,22 +1533,6 @@ files = [
[package.extras]
png = ["pypng (>=0.0.13)"]
[[package]]
name = "pyscss"
version = "1.4.0"
description = "pyScss, a Scss compiler for Python"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "pyScss-1.4.0.tar.gz", hash = "sha256:8f35521ffe36afa8b34c7d6f3195088a7057c185c2b8f15ee459ab19748669ff"},
]
[package.dependencies]
enum34 = "*"
pathlib2 = "*"
six = "*"
[[package]]
name = "pysocks"
version = "1.7.1"
@@ -1607,6 +1634,56 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
{file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
{file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
{file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
{file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
{file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
[[package]]
name = "represent"
version = "1.6.0.post0"
@@ -1886,6 +1963,18 @@ files = [
{file = "tomlkit-0.11.7.tar.gz", hash = "sha256:f392ef70ad87a672f02519f99967d28a4d3047133e2d1df936511465fbb3791d"},
]
[[package]]
name = "types-mock"
version = "5.0.0.6"
description = "Typing stubs for mock"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "types-mock-5.0.0.6.tar.gz", hash = "sha256:3abdee238948c6678ef5b4fd740bd41d30289a2fe4d91aac4ae9f6869e6c9333"},
{file = "types_mock-5.0.0.6-py3-none-any.whl", hash = "sha256:6946f4b2240160a1740db7d6c8e8736705acd133afd98555897db0116d41a6fc"},
]
[[package]]
name = "types-protobuf"
version = "3.20.4.6"
@@ -1977,6 +2066,27 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flak
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=19.0.0,<19.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"]
[[package]]
name = "virtualenv"
version = "20.22.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
{file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
]
[package.dependencies]
distlib = ">=0.3.6,<1"
filelock = ">=3.11,<4"
platformdirs = ">=3.2,<4"
[package.extras]
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]]
name = "websocket-client"
version = "1.3.3"
@@ -2162,5 +2272,5 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.10 | ^3.9 | ^3.8.1"
content-hash = "d8cae4bcda9fcc8db58c08e1027d1e735dfe87e77584d7763348a6183c0fbad8"
python-versions = "^3.10 | ^3.9"
content-hash = "23981f78fb5f0da54fa4185d4fe7c009eda6b37a9372e7a471b6e10dc1058497"
+6 -10
View File
@@ -1,15 +1,11 @@
[tool.poetry]
name = "lnbits"
version = "0.10.4.1"
version = "0.10.8"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
[tool.poetry.build]
generate-setup-file = false
script = "tools/build.py"
[tool.poetry.dependencies]
python = "^3.10 | ^3.9 | ^3.8.1"
python = "^3.10 | ^3.9"
bech32 = "1.2.0"
bitstring = "3.1.9"
click = "8.0.4"
@@ -22,7 +18,6 @@ lnurl = "0.3.6"
psycopg2-binary = "2.9.1"
pydantic = "1.10.4"
pyqrcode = "1.2.1"
pyScss = "1.4.0"
shortuuid = "1.0.11"
sqlalchemy = "1.3.24"
sqlalchemy-aio = "0.17.0"
@@ -37,12 +32,11 @@ protobuf = "4.21.12"
Cerberus = "1.3.4"
async-timeout = "4.0.2"
pyln-client = "0.11.1"
boltz-client = "0.1.3"
cashu = "0.9.0"
[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
isort = "^5.10.1"
isort = "^5.12.0"
pytest = "^7.1.2"
mock = "^4.0.3"
black = "^22.6.0"
@@ -51,9 +45,11 @@ pytest-cov = "^3.0.0"
mypy = "^0.971"
types-protobuf = "^3.19.22"
pylint = "^2.17.2"
pre-commit = "^3.2.2"
types-mock = "^5.0.0.6"
[build-system]
requires = ["poetry-core>=1.0.0", "pyscss"]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
+65
View File
@@ -1,10 +1,15 @@
import asyncio
import hashlib
from time import time
import pytest
from lnbits import bolt11
from lnbits.core.models import Payment
from lnbits.core.views.api import api_payment
from lnbits.db import DB_TYPE, SQLITE
from lnbits.settings import get_wallet_class
from tests.conftest import CreateInvoiceData, api_payments_create_invoice
from ...helpers import get_random_invoice_data, is_fake
@@ -181,6 +186,66 @@ async def test_pay_invoice_adminkey(client, invoice, adminkey_headers_from):
assert response.status_code > 300 # should fail
@pytest.mark.asyncio
async def test_get_payments(client, from_wallet, adminkey_headers_from):
# Because sqlite only stores timestamps with milliseconds we have to wait a second to ensure
# a different timestamp than previous invoices
# due to this limitation both payments (normal and paginated) are tested at the same time as they are almost
# identical anyways
if DB_TYPE == SQLITE:
await asyncio.sleep(1)
ts = time()
fake_data = [
CreateInvoiceData(amount=10, memo="aaaa"),
CreateInvoiceData(amount=100, memo="bbbb"),
CreateInvoiceData(amount=1000, memo="aabb"),
]
for invoice in fake_data:
await api_payments_create_invoice(invoice, from_wallet)
async def get_payments(params: dict):
params["time[ge]"] = ts
response = await client.get(
"/api/v1/payments",
params=params,
headers=adminkey_headers_from,
)
assert response.status_code == 200
return [Payment(**payment) for payment in response.json()]
payments = await get_payments({"sortby": "amount", "direction": "desc", "limit": 2})
assert payments[-1].amount < payments[0].amount
assert len(payments) == 2
payments = await get_payments({"offset": 2, "limit": 2})
assert len(payments) == 1
payments = await get_payments({"sortby": "amount", "direction": "asc"})
assert payments[-1].amount > payments[0].amount
payments = await get_payments({"search": "aaa"})
assert len(payments) == 1
payments = await get_payments({"search": "aa"})
assert len(payments) == 2
# amount is in msat
payments = await get_payments({"amount[gt]": 10000})
assert len(payments) == 2
response = await client.get(
"/api/v1/payments/paginated",
params={"limit": 2, "time[ge]": ts},
headers=adminkey_headers_from,
)
assert response.status_code == 200
paginated = response.json()
assert len(paginated["data"]) == 2
assert paginated["total"] == len(fake_data)
# check POST /api/v1/payments/decode
@pytest.mark.asyncio
async def test_decode_invoice(client, invoice):
-63
View File
@@ -1,63 +0,0 @@
import os
import warnings
from pathlib import Path
LNBITS_PATH = Path("lnbits").absolute()
# from ..lnbits.helpers import vendored_js, vendored_css
vendored_js = [
"/static/vendor/moment.js",
"/static/vendor/underscore.js",
"/static/vendor/axios.js",
"/static/vendor/vue.js",
"/static/vendor/vue-router.js",
"/static/vendor/vue-qrcode-reader.browser.js",
"/static/vendor/vue-qrcode.js",
"/static/vendor/vue-i18n.js",
"/static/vendor/vuex.js",
"/static/vendor/quasar.ie.polyfills.umd.min.js",
"/static/vendor/quasar.umd.js",
"/static/vendor/Chart.bundle.js",
]
vendored_css = [
"/static/vendor/quasar.css",
"/static/vendor/Chart.css",
"/static/vendor/vue-qrcode-reader.css",
]
def url_for_vendored(abspath: str) -> str:
return f"/{os.path.relpath(abspath, LNBITS_PATH)}"
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for files, outputpath in [
(vendored_js, os.path.join(LNBITS_PATH, "static/bundle.js")),
(vendored_css, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in files:
with open(f"{LNBITS_PATH}{path}") as f:
output += f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
def build():
transpile_scss()
bundle_vendored()
if __name__ == "__main__":
build()