Compare commits

..
163 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
ArcandGitHub be1d515307 Merge pull request #1642 from lnbits/new-version
change version to 0.10.4
2023-04-21 13:15:28 +01:00
dni ⚡ 87d1de2a31 change version to 0.10.4.1 2023-04-21 13:18:03 +02:00
dni ⚡ 0546fb0a77 change version to 0.10.4 2023-04-21 13:08:01 +02:00
ArcandGitHub efafd00b43 Merge pull request #1621 from lnbits/fix_adminui_lnd
fix inputs on Lnd funding sources
2023-04-20 09:51:19 +01:00
ArcandGitHub 07946d873f Merge pull request #1630 from lnbits/update/flake8
FEAT: improve on flake8 linting
2023-04-20 09:49:11 +01:00
ArcandGitHub 91b28ebf83 Merge pull request #1612 from lnbits/fix_admin_ext_access
Fix admin ext access
2023-04-20 09:46:18 +01:00
ArcandGitHub dfb6378976 Merge pull request #1627 from lnbits/fix/formatting
formatting did only run on branch main?
2023-04-20 09:41:17 +01:00
ArcandGitHub 64fde0093c Merge pull request #1636 from lnbits/improve-errors-lndrest
BUG: SSL certificate for lndrest is required even though it could be a publicly issued one
2023-04-20 09:39:52 +01:00
ArcandGitHub c8f9473e01 Merge pull request #1634 from lnbits/allow_bip21_scan
Allow for scanning BIP21 QR codes
2023-04-20 09:36:52 +01:00
ArcandGitHub fea6183aef Merge pull request #1608 from lnbits/feat/i18n
FEAT: add localisation to lnbits frontend
2023-04-20 09:25:35 +01:00
dni ⚡ adae3b4302 optional ssl certifiace for lndrest 2023-04-19 10:51:00 +02:00
Tiago Vasconcelos ce82439fd8 format 2023-04-18 17:02:49 +01:00
Tiago Vasconcelos 0a210f766d allow to scan BIP21 QR codes 2023-04-18 15:39:53 +01:00
dni ⚡andGitHub fe315d0a9b FEAT: remove python 3.7 and update dependencies (#1629)
* FEAT: remove python 3.7 and update dependencies

* readd pylint
2023-04-17 09:44:08 +02:00
dni ⚡andGitHub 156a19f05c improve errorhandling for lndrest #252 (#1631) 2023-04-17 09:26:37 +02:00
dni ⚡ 06fa6c10c1 reintroduce E203, black does not like that one 2023-04-17 08:38:12 +02:00
dni ⚡ 0b596c00ca reduce max string length 2023-04-17 08:29:01 +02:00
dni ⚡ 0af22a6256 E471 2023-04-17 08:21:45 +02:00
dni ⚡ 1ac9d2572d E203, E221 2023-04-17 08:16:59 +02:00
dni ⚡ 4a72a5b6b3 E241, E501 2023-04-17 08:14:04 +02:00
dni ⚡ b465ecf22a E265 2023-04-17 08:11:07 +02:00
dni ⚡ 42b7cd3046 fix E266 2023-04-17 08:09:34 +02:00
dni ⚡ 45b36b896b move flake config from setup.cfg to .flake8, to not confuse it with setup.cfg python package settings 2023-04-17 08:08:16 +02:00
dni ⚡ 99964ee6ab formatting did only run on branch main? 2023-04-16 09:16:24 +02:00
ArcandGitHub d82118db20 Merge pull request #1623 from lnbits/bug/fake_wallet_invoice
hotfix fakewallet
2023-04-14 11:13:25 +01:00
dni ⚡ ffb0f4ccb0 hotfix fakewallet 2023-04-14 12:03:06 +02:00
Tiago Vasconcelos de38f38723 fix inputs on Lnd funding sources 2023-04-13 11:43:19 +01:00
dni ⚡ 8c3b4a87b1 add i18n to frontend, still incomplete, but a start 2023-04-13 09:32:12 +02:00
dni ⚡ 8ee6be30eb add vue-i18n dependecy 2023-04-13 07:59:12 +02:00
dni ⚡andGitHub cf3b169480 FEAT: ignore extensions for checks, linting, prettier (#1617) 2023-04-13 07:57:14 +02:00
ArcandGitHub 95a718c6c0 Merge pull request #1614 from lnbits/arcbtc-patch-1
Link install guide
2023-04-11 17:29:18 +01:00
dni ⚡andGitHub 2473e0e962 FEAT: lock name inside package-lock, by adding name to package.json (#1616) 2023-04-11 10:31:44 +02:00
ArcandGitHub 90d1e1ebb6 Update README.md 2023-04-08 14:53:49 +01:00
Vlad Stan 554ad88cbd doc: add comments 2023-04-06 12:32:23 +03:00
Vlad Stan 0d14d2b56e fix: remove admin extensions for non admin users 2023-04-05 19:40:16 +03:00
Vlad Stan d7e7d89e9a fix: mypy 2023-04-05 19:03:14 +03:00
Vlad Stan 1d0fcaa579 feat: show content by accepted type 2023-04-05 18:54:54 +03:00
Vlad Stan af212c820c fix: block access admin extensions to normal users 2023-04-05 17:40:08 +03:00
Vlad Stan 0a74ca3972 fix: use extension code instead on name 2023-04-05 17:39:22 +03:00
ArcandGitHub 47df94178e Merge pull request #1468 from lnbits/pyright3
introduce pyright + fix issues (supersedes #1444)
2023-04-05 12:42:23 +01:00
dni ⚡ 255638a9ce remove elif for vlad 2023-04-05 11:39:01 +02:00
dni ⚡ 50149c360c add assert 2023-04-05 11:38:07 +02:00
dni ⚡ 6fae6dab92 remove *1000 for fake 2023-04-05 11:35:09 +02:00
dni ⚡ be5f909d1c merge issue in lnd_rest 2023-04-04 08:00:43 +02:00
dni ⚡ 2a1a0f3a2f fix pyright issues 2023-04-04 07:49:13 +02:00
dni ⚡ 5dbd9a9192 add some assert description 2023-04-04 07:35:20 +02:00
dni ⚡ 1f6a961840 fix cln mypy 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ 9b7bda4e4e finish services.py 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ a8103ba091 remove unnecessary checking_id from fakewallet 2023-04-04 07:35:20 +02:00
callebtcanddni ⚡ 234b508368 shorten and ignore cln.py 2023-04-04 07:35:19 +02:00
callebtcanddni ⚡ f766f9b2c3 comment 2023-04-04 07:35:05 +02:00
callebtcanddni ⚡ 370ec7e366 remove error fallback 2023-04-04 07:35:04 +02:00
dni ⚡ 0822058661 fix newly introduced pyright issue from rebase 2023-04-04 07:35:04 +02:00
dni ⚡ 7be46587db do not ignore core and wallets for pyright, bug probably got introduced while rebasing... 2023-04-04 07:35:04 +02:00
dni ⚡ fa58a5fa4c flake it 2023-04-04 07:35:04 +02:00
dni ⚡ 486631b455 move middleware to middleware.py 2023-04-04 07:35:04 +02:00
dni ⚡ 435743a265 add **kwargs to create_invoice functions 2023-04-04 07:34:51 +02:00
dni ⚡ 72386033a9 formatting 2023-04-04 07:34:51 +02:00
dni ⚡ 69c0cd0276 asserting broke payments 2023-04-04 07:34:51 +02:00
dni ⚡ abfee29676 uncomment webhook_listerner and add TODO 2023-04-04 07:34:50 +02:00
Pavol Rusnakanddni ⚡ 9c8a79eb9b fix pyright lnbits/wallets
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:50 +02:00
Pavol Rusnakanddni ⚡ 0607cb1d6e fix pyright lnbits/core
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:37 +02:00
Pavol Rusnakanddni ⚡ 02306148df fix pyright lnbits
Co-authored-by: dni  <office@dnilabs.com>
2023-04-04 07:34:17 +02:00
Pavol RusnakandGitHub 3855cf47f3 Merge pull request #1607 from lnbits/f-strings
small followup to #1597
2023-04-03 17:59:50 +02:00
Pavol Rusnak b2bc378a6f small followup to #1597 2023-04-03 17:54:55 +02:00
e59a218912 bugfix: 500 error if same wallet tries to pay an external invoice twice (#1594)
* bugfix: 500 error if same wallet tries to pay an external invoice twice

* fstring

* log the error

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-04-03 15:34:55 +02:00
689c443d72 increase readability by using f-strings (#1597)
* increase readability by using f-strings

* readd merge conflict stuff

---------

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-04-03 15:34:17 +02:00
calleandGitHub 58c3b05e29 fix internal payment check (#1604) 2023-04-03 15:15:34 +02:00
8ce84ce592 FEAT: Filters for GET requests, add it to GET /payments (#1557)
* feat filters, add them to GET payments

* add limit and offset to filters (#1563)

* add limit and offset to filters
* move filters example to parse_filters doc string

* black

* add openapi docs

* remove example commentC

* improve typing and make nested filter possible in openapi

* typo in fn name

* readd Type

---------

Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-04-03 14:55:49 +02:00
fe9e821af5 BUG: proper exception for already paid internal invoices (#1593)
* BUG: proper exception for paid interal invoices

* test should now fail, because we pay invoice twice, and should also work in regtest

* sorting

* unpack check_internal

* introduce another crud fn for checking internal paid payment

* rename

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-04-03 14:44:17 +02:00
dni ⚡andGitHub a9bdf24425 FEAT: use versioning for frontend (npm) and copy it to lnbits/static/vendor for easier updating (#1590)
* remove static/vendor

* add node dependencies

* add bolt11-decoder

* run npm install inside dockerimage

* only use bundle.js and bundle.css

* use node_modules for bundling vendor assets

* remove dead code

* make argument optional

* reintroduce vendor dir

* reintroduce vendor and single javascript files, minification

* wrong moment, remove minification

* lock packages with non critical issues

* black
2023-03-31 12:46:24 +02:00
dni ⚡andGitHub 43c9c9754b FEAT: #1552 add copy payment_hash to payment details (#1591)
* add copy to payment details

* icon for calle
2023-03-30 14:30:50 +02:00
dni ⚡andGitHub 9127ebc621 FIX: admin topup to use payment_hash (#1588)
* FIX: admin topup to use payment_hash

* flake8
2023-03-30 14:21:01 +02:00
dni ⚡andGitHub 407a61cd29 add version to fastapi app and startup log (#1568)
* add version to fastapi app and startup log

* move version into settings

* remove unused dep
2023-03-30 14:19:45 +02:00
calleandGitHub 994b203a8e return fee in status (#1598) 2023-03-29 19:40:52 +02:00
Vlad Stan 239f166a14 fix: format relative path 2023-03-20 16:07:20 +02:00
Vlad Stan e1e34def8c fix: correct method names for Path 2023-03-20 16:07:20 +02:00
Pavol RusnakandVlad Stan a805d2a0e8 refactor extension_manager to use pathlib.Path instead of os.path.join 2023-03-20 16:07:20 +02:00
Pavol RusnakandGitHub 281d37f79c Merge pull request #1582 from lnbits/docs-nginx
docs: add nginx ssl proxy info to installation
2023-03-19 09:27:14 +01:00
Pavol RusnakandGitHub 0039c60a27 Merge pull request #1583 from lnbits/mypy-pyproject-simplify
simplify exclude syntax for mypy in pyproject.toml
2023-03-18 20:49:23 +01:00
Pavol RusnakandGitHub 3317bc1bc5 Merge pull request #1578 from lnbits/flake-update
flake: update to latest nixpkgs
2023-03-17 23:10:55 +01:00
Pavol Rusnak 9cf43dcea1 simplify exclude syntax for mypy in pyproject.toml 2023-03-17 18:05:43 +01:00
Pavol Rusnak b3910da6d7 flake: update to latest nixpkgs 2023-03-17 18:03:33 +01:00
Pavol Rusnak ec1caa6015 docs: fix readability of HTTPS section in installation 2023-03-17 15:13:25 +01:00
Pavol Rusnak 0d8d580fc9 docs: add nginx ssl proxy info to installation 2023-03-17 15:10:15 +01:00
4dd206da2a good bye venv (#1569)
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-03-16 16:45:23 +01:00
Vlad StanandGitHub fbe982af36 fix: assume no pre-installed extensions (#1558)
* fix: assume no pre-installed extensions

* chore: code format
2023-03-15 11:26:07 +01:00
ArcandGitHub a360a47be9 Merge pull request #1577 from lnbits/bump-version-0-10-3
bump to version 0.10.3
2023-03-10 13:03:00 +00:00
dni ⚡ 552f253b5d bump to version 0.10.3 2023-03-10 14:01:46 +01:00
ArcandGitHub 5021d86488 Merge pull request #1566 from bitkarrot/patch-3
update installation.md path
2023-03-10 13:01:22 +00:00
ArcandGitHub c257e42222 Merge pull request #1575 from wazint/ext-doc
Doc: Fix dev guide for getting started with extensions
2023-03-10 13:00:15 +00:00
ArcandGitHub 17cdf57b8f Merge pull request #1576 from prusnak/shortuuid-bump
Bump shortuuid dependency to fix build on Python 3.10
2023-03-10 12:59:04 +00:00
ArcandGitHub 375b575eed Merge pull request #1572 from lnbits/readme-wiki-link
fix markdown links in the readme
2023-03-10 12:58:28 +00:00
Pavol Rusnak f860c170fc Bump shortuuid dependency to fix build on Python 3.10 2023-03-10 11:54:23 +01:00
ArcandGitHub 095206e2ed Merge pull request #1574 from lnbits/fix-flake
fix flake error: #1088
2023-03-10 10:54:18 +00:00
ArcandGitHub 4f3159339a Merge pull request #1573 from lnbits/feat-backup-pg_dump
BUG: Dockerfile does not support backup funcionality
2023-03-10 09:58:53 +00:00
wazint 9ba2983572 Merge branch 'ext-doc' of github-tor:wazint/lnbits into ext-doc 2023-03-09 21:55:26 +02:00
wazint acaf4849d2 doc: Fix dev guide for getting started with extensions 2023-03-09 21:53:11 +02:00
dni ⚡ 3fa4135a12 non interactive install 2023-03-09 16:01:52 +01:00
dni ⚡ a8f534cd2e postgres-client version 14... 2023-03-09 15:10:50 +01:00
dni ⚡ c58fac39cd fix flake error: #1088 2023-03-09 14:50:11 +01:00
dni ⚡ 161cb9cfbc remove build not needed, create the right datadir, add postgres-client for pg_dump backup functionality 2023-03-09 13:48:44 +01:00
Pavol Rusnak 892d8ffa62 fix markdown links in the readme 2023-03-08 18:15:58 +01:00
BitkarrotandGitHub 3d55d98d33 update installation.md
lnbits-legend -> lnbits for systemd
2023-03-07 20:14:15 -08:00
wazint 1e39e7dd63 doc: Fix dev guide for getting started with extensions 2023-03-01 23:06:44 +02:00
126 changed files with 68844 additions and 2396 deletions
-1
View File
@@ -3,7 +3,6 @@ data
docker
docs
tests
venv
lnbits/static/css/*
lnbits/static/bundle.js
+16
View File
@@ -0,0 +1,16 @@
[flake8]
max-line-length = 150
exclude = lnbits/wallets/lnd_grpc_files/, lnbits/extensions/
ignore =
# E203 whitespace before ':' black does not like it
E203
# E402: module level import not at top of file
E402,
# W503: line break before binary operator
W503,
# F821: undefined name - should be addressed in future PR
F821,
# E722 do not use bare 'except' - should be addressed in future PR
E722,
# flake8-requirements import checks
I
+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: ''
---
+1 -5
View File
@@ -1,10 +1,6 @@
name: formatting
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [push, pull_request]
jobs:
checks:
-23
View File
@@ -3,29 +3,6 @@ name: tests
on: [push, pull_request]
jobs:
venv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
run: make test-venv
sqlite:
runs-on: ubuntu-latest
strategy:
+3 -3
View File
@@ -23,8 +23,7 @@ tests/data/*.sqlite3
*.pyc
*.env
.env
.venv
venv
.super_user
data
*.sqlite3
@@ -34,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
+2
View File
@@ -5,6 +5,8 @@
*.yml
**/lnbits/extensions/*
**/lnbits/static/vendor
**/lnbits/static/bundle.*
**/lnbits/static/css/*
+10 -3
View File
@@ -3,18 +3,25 @@ FROM python:3.10-slim
RUN apt-get clean
RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# needed for backups postgresql-client version 14 (pg_dump)
RUN apt-get install -y wget
RUN echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN apt-get update
RUN apt-get install -y postgresql-client-14
WORKDIR /app
RUN mkdir -p lnbits/data
COPY . .
RUN mkdir data
RUN poetry config virtualenvs.create false
RUN poetry install --only main
RUN poetry run python tools/build.py
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
+27 -9
View File
@@ -1,6 +1,6 @@
.PHONY: test
all: format check requirements.txt
all: format check
format: prettier isort black
@@ -50,14 +50,6 @@ test-real-wallet:
DEBUG=true \
poetry run pytest
test-venv:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
@@ -79,3 +71,29 @@ migration:
bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
#
sass:
npm run sass
bundle:
npm install
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
+9 -12
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,15 +15,15 @@ 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.
See [LNbits Wiki](github.com/lnbits/lnbits/wiki/) for more detailed documentation.
See [LNbits Wiki](https://github.com/lnbits/lnbits/wiki/) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
@@ -33,7 +31,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits
See the [install guide](github.com/lnbits/lnbits/wiki/) for details on installation and setup.
See the [install guide](https://github.com/lnbits/lnbits/wiki/LNbits-Documentation#install-options) for details on installation and setup.
## LNbits as an account system
@@ -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
+9 -8
View File
@@ -9,11 +9,14 @@ nav_order: 2
Making extensions
=================
Start off by copying the example extension in `lnbits/extensions/example` into your own:
Start off by cloning the [example extension](https://github.com/lnbits/example) into your `lnbits/extensions` folder and renaming it to `mysuperplugin`:
```sh
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
cd lnbits/extensions/mysuperplugin
cd lnbits/extensions
git clone https://github.com/lnbits/example.git --depth=1 mysuperplugin # Let's not use dashes or anything; it doesn't like those.
cd mysuperplugin
rm -rf .git/
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
mv templates/example templates/mysuperplugin # Rename templates folder.
```
- if you are on macOS and having difficulty with 'sed', consider `brew install gnu-sed` and use 'gsed', without -0 option after xargs.
@@ -28,18 +31,16 @@ Going over the example extension's structure:
Adding new dependencies
-----------------------
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `venv`, or `poerty`:
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`:
```sh
$ poetry add <package>
# or
$ ./venv/bin/pip install <package>
```
**But we need an extra step to make sure LNbits doesn't break in production.**
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
SQLite to PostgreSQL migration
+2 -2
View File
@@ -1,6 +1,6 @@
# Extension Install
Anyone can create an extension by following the [example extension](https://github.com/lnbits/lnbits/tree/extension_install_02/lnbits/extensions/example).
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
Extensions can be installed by an admin user after the **LNbits** instance has been started.
@@ -12,7 +12,7 @@ Go to `Manage Server` > `Server` > `Extensions Manifests`
An `Extension Manifest` is a link to a `JSON` file which contains information about various extensions that can be installed (repository of extensions).
Multiple repositories can be configured. For more information check the [Manifest File](https://github.com/lnbits/lnbits/blob/extension_install_02/docs/guide/extension-install.md#manifest-file) section.
Multiple repositories can be configured. For more information check the [Manifest File](https://github.com/lnbits/lnbits/blob/main/docs/guide/extension-install.md#manifest-file) section.
**LNbits** administrators should configure their instances to use repositories that they trust (like the [lnbits-extensions](https://github.com/lnbits/lnbits-extensions/) one).
+72 -42
View File
@@ -6,13 +6,13 @@ nav_order: 2
# Basic installation
You can choose between four package managers, `poetry`, `nix` and `venv`.
You can choose between four package managers, `poetry` and `nix`
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
## 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
@@ -80,30 +80,8 @@ mkdir data
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
## Option 3: venv
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3.9-venv'
python3.9 -m venv venv
# If you have problems here, try `sudo apt install -y pkg-config libpq-dev`
./venv/bin/pip install -r requirements.txt
# create the data folder and the .env file
mkdir data && cp .env.example .env
# build the static files
./venv/bin/python tools/build.py
```
#### Running the server
```sh
./venv/bin/uvicorn lnbits.__main__:app --port 5000
```
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
## Option 4: Docker
## Option 3: Docker
use latest version from docker hub
```sh
@@ -122,7 +100,7 @@ mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
```
## Option 5: Fly.io
## Option 4: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
@@ -210,15 +188,8 @@ sudo apt install python3.9-dev gcc build-essential
# if the secp256k1 build fails:
# if you used poetry
poetry add setuptools wheel
# if you used venv
./venv/bin/pip install 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:
@@ -303,7 +274,7 @@ Description=LNbits
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits-legend
WorkingDirectory=/home/lnbits/lnbits
# same here. run `which poetry` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
# replace with the user that you're running lnbits on
@@ -323,7 +294,8 @@ Save the file and run the following commands:
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## Reverse proxy with automatic https using Caddy
## Reverse proxy with automatic HTTPS using Caddy
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
@@ -335,11 +307,15 @@ https://caddyserver.com/docs/install#debian-ubuntu-raspbian
```
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Assuming your LNbits is running on port `5000` add:
```
yourdomain.com {
handle /api/v1/payments/sse* {
@@ -356,22 +332,30 @@ yourdomain.com {
}
}
```
Save and exit `CTRL + x`
```
sudo caddy start
```
## Running behind an apache2 reverse proxy over https
Install apache2 and enable apache2 mods
## Running behind an Apache2 reverse proxy over HTTPS
Install Apache2 and enable Apache2 mods:
```sh
apt-get install apache2 certbot
a2enmod headers ssl proxy proxy-http
```
create a ssl certificate with letsencrypt
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --webroot --agree-tos --text --non-interactive --webroot-path /var/www/html -d lnbits.org
certbot certonly --webroot --agree-tos --non-interactive --webroot-path /var/www/html -d lnbits.org
```
create a apache2 vhost at: /etc/apache2/sites-enabled/lnbits.conf
Create an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
```sh
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
<VirtualHost *:443>
@@ -396,11 +380,57 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
</VirtualHost>
EOF
```
restart apache2
Restart Apache2:
```sh
service restart apache2
```
## Running behind an Nginx reverse proxy over HTTPS
Install nginx:
```sh
apt-get install nginx certbot
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --nginx --agree-tos -d lnbits.org
```
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
```sh
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
server {
server_name lnbits.org;
location / {
proxy_pass http://127.0.0.1:5000;
}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lnbits.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
EOF
```
Restart nginx:
```sh
service restart nginx
```
## Using https without reverse proxy
The most common way of using LNbits via https is to use a reverse proxy such as Caddy, nginx, or ngriok. However, you can also run LNbits via https without additional software. This is useful for development purposes or if you want to use LNbits in your local network.
@@ -433,7 +463,7 @@ mkcert localhost 127.0.0.1 ::1
You can then pass the certificate files to uvicorn when you start LNbits:
```sh
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
```
+3 -3
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
@@ -47,7 +47,7 @@ You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
@@ -57,7 +57,7 @@ To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroo
### LNPay
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
Generated
+12 -25
View File
@@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"lastModified": 1678470307,
"narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f",
"type": "github"
},
"original": {
@@ -31,32 +31,19 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1657261001,
"narHash": "sha256-sUZeuRYfhG59uD6xafM07bc7bAIkpcGq84Vj4B+cyms=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0be91cefefde5701f8fa957904618a13e3bb51d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"lastModified": 1678513806,
"narHash": "sha256-bEto1lp9bIQ+DlJsXQyRxN5l6B/oy3Skb7DueYRJJBo=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"rev": "bf239d53fafb53cf439a72b3a50e86dd6a3984a5",
"type": "github"
},
"original": {
+7 -2
View File
@@ -1,7 +1,12 @@
{
description = "LNbits, free and open-source Lightning wallet and accounts system";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
@@ -17,6 +22,7 @@
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
poetry
];
};
});
@@ -29,7 +35,6 @@
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
python = pkgs.python39;
};
});
nixosModules = {
+14 -24
View File
@@ -33,12 +33,7 @@ from .core import (
from .core.services import check_admin_settings
from .core.views.generic import core_html_routes
from .extension_manager import Extension, InstallableExtension, get_valid_extensions
from .helpers import (
get_css_vendored,
get_js_vendored,
template_renderer,
url_for_vendored,
)
from .helpers import template_renderer
from .middleware import ExtensionsRedirectMiddleware, InstalledExtensionMiddleware
from .requestvars import g
from .tasks import (
@@ -57,6 +52,7 @@ def create_app() -> FastAPI:
app = FastAPI(
title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
version=settings.version,
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
@@ -83,7 +79,6 @@ def create_app() -> FastAPI:
app.add_middleware(ExtensionsRedirectMiddleware)
register_startup(app)
register_assets(app)
register_routes(app)
register_async_tasks(app)
register_exception_handlers(app)
@@ -162,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]:
@@ -202,7 +201,7 @@ def check_installed_extension(ext: InstallableExtension) -> bool:
os.path.join(settings.lnbits_data_folder, "extensions", "*.zip")
)
if ext.zip_path not in zip_files:
if f"./{str(ext.zip_path)}" not in zip_files:
ext.download_archive()
ext.extract_archive()
@@ -305,6 +304,7 @@ def register_startup(app: FastAPI):
def log_server_info():
logger.info("Starting LNbits")
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
logger.info(f"Port: {settings.port}")
@@ -328,19 +328,6 @@ def get_db_vendor_name():
)
def register_assets(app: FastAPI):
"""Serve each vendored asset separately or a bundle."""
@app.on_event("startup")
async def vendored_assets_variable():
if settings.debug:
g().VENDORED_JS = map(url_for_vendored, get_js_vendored())
g().VENDORED_CSS = map(url_for_vendored, get_css_vendored())
else:
g().VENDORED_JS = ["/static/bundle.js"]
g().VENDORED_CSS = ["/static/bundle.css"]
def register_async_tasks(app):
@app.route("/wallet/webhook")
async def webhook_listener():
@@ -445,7 +432,10 @@ class Formatter:
self.padding = 0
self.minimal_fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>\n"
if settings.debug:
self.fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
self.fmt: str = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
)
else:
self.fmt: str = self.minimal_fmt
+33 -30
View File
@@ -66,11 +66,12 @@ def decode(pr: str) -> Invoice:
invoice.amount_msat = _unshorten_amount(amountstr)
# pull out date
invoice.date = data.read(35).uint
date_bin = data.read(35)
invoice.date = date_bin.uint # type: ignore
while data.pos != data.len:
tag, tagdata, data = _pull_tagged(data)
data_length = len(tagdata) / 5
data_length = len(tagdata or []) / 5
if tag == "d":
invoice.description = _trim_to_bytes(tagdata).decode()
@@ -79,7 +80,7 @@ def decode(pr: str) -> Invoice:
elif tag == "p" and data_length == 52:
invoice.payment_hash = _trim_to_bytes(tagdata).hex()
elif tag == "x":
invoice.expiry = tagdata.uint
invoice.expiry = tagdata.uint # type: ignore
elif tag == "n":
invoice.payee = _trim_to_bytes(tagdata).hex()
# this won't work in most cases, we must extract the payee
@@ -90,11 +91,11 @@ def decode(pr: str) -> Invoice:
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route(
pubkey=s.read(264).tobytes().hex(),
short_channel_id=_readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe,
pubkey=s.read(264).tobytes().hex(), # type: ignore
short_channel_id=_readable_scid(s.read(64).intbe), # type: ignore
base_fee_msat=s.read(32).intbe, # type: ignore
ppm_fee=s.read(32).intbe, # type: ignore
cltv=s.read(16).intbe, # type: ignore
)
invoice.route_hints.append(route)
@@ -171,7 +172,7 @@ def lnencode(addr, privkey):
else:
amount = addr.currency if addr.currency else ""
hrp = "ln" + amount + "0n"
hrp = f"ln{amount}0n"
# Start with the timestamp
data = bitstring.pack("uint:35", addr.date)
@@ -202,7 +203,8 @@ def lnencode(addr, privkey):
)
data += tagged("r", route)
elif k == "f":
data += encode_fallback(v, addr.currency)
# NOTE: there was an error fallback here that's now removed
continue
elif k == "d":
data += tagged_bytes("d", v.encode())
elif k == "x":
@@ -244,7 +246,13 @@ def lnencode(addr, privkey):
class LnAddr:
def __init__(
self, paymenthash=None, amount=None, currency="bc", tags=None, date=None
self,
paymenthash=None,
amount=None,
currency="bc",
tags=None,
date=None,
fallback=None,
):
self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags
@@ -252,12 +260,14 @@ class LnAddr:
self.paymenthash = paymenthash
self.signature = None
self.pubkey = None
self.fallback = fallback
self.currency = currency
self.amount = amount
def __str__(self):
pubkey = bytes.hex(self.pubkey.serialize()).decode()
tags = ", ".join([k + "=" + str(v) for k, v in self.tags])
assert self.pubkey, "LnAddr, pubkey must be set"
pubkey = bytes.hex(self.pubkey.serialize())
tags = ", ".join([f"{k}={v}" for k, v in self.tags])
return f"LnAddr[{pubkey}, amount={self.amount}{self.currency} tags=[{tags}]]"
@@ -266,6 +276,7 @@ def shorten_amount(amount):
# Convert to pico initially
amount = int(amount * 10**12)
units = ["p", "n", "u", "m", ""]
unit = ""
for unit in units:
if amount % 1000 == 0:
amount //= 1000
@@ -304,32 +315,24 @@ def _pull_tagged(stream):
return (CHARSET[tag], stream.read(length * 5), stream)
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray
def tagged(char, l):
def tagged(char, bits):
# Tagged fields need to be zero-padded to 5 bits.
while l.len % 5 != 0:
l.append("0b0")
while bits.len % 5 != 0:
bits.append("0b0")
return (
bitstring.pack(
"uint:5, uint:5, uint:5",
CHARSET.find(char),
(l.len / 5) / 32,
(l.len / 5) % 32,
(bits.len / 5) / 32,
(bits.len / 5) % 32,
)
+ l
+ bits
)
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
def tagged_bytes(char, bits):
return tagged(char, bitstring.BitArray(bits))
def _trim_to_bytes(barr):
@@ -359,5 +362,5 @@ def bitarray_to_u5(barr):
ret = []
s = bitstring.ConstBitStream(barr)
while s.pos != s.len:
ret.append(s.read(5).uint)
ret.append(s.read(5).uint) # type: ignore
return ret
+1 -34
View File
@@ -1,6 +1,4 @@
import asyncio
import os
import warnings
import click
from loguru import logger
@@ -13,7 +11,6 @@ from .core.crud import get_dbversions, get_inactive_extensions
from .core.helpers import migrate_extension_database, run_migration
from .db import COCKROACH, POSTGRES, SQLITE
from .extension_manager import get_valid_extensions
from .helpers import get_css_vendored, get_js_vendored, url_for_vendored
@click.command("migrate")
@@ -21,41 +18,11 @@ def db_migrate():
asyncio.create_task(migrate_databases())
@click.command("assets")
def handle_assets():
transpile_scss()
bundle_vendored()
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()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(settings.lnbits_path, "static/bundle.js")),
(get_css_vendored, os.path.join(settings.lnbits_path, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
async with core_db.connect() as conn:
exists = False
if conn.type == SQLITE:
exists = await conn.fetchone(
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
+95 -47
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
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)
@@ -62,7 +69,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
return User(
id=user["id"],
email=user["email"],
extensions=[e[0] for e in extensions],
extensions=[
e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"])
],
wallets=[Wallet(**w) for w in wallets],
admin=user["id"] == settings.super_user
or user["id"] in settings.lnbits_admin_users,
@@ -206,7 +215,7 @@ async def create_wallet(
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
return await (conn or db).execute(
await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
@@ -214,6 +223,9 @@ async def update_wallet(
""",
(new_name, wallet_id),
)
wallet = await get_wallet(wallet_id=wallet_id, conn=conn)
assert wallet, "updated created wallet couldn't be retrieved"
return wallet
async def delete_wallet(
@@ -324,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}
@@ -338,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,
@@ -347,29 +359,23 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = 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
@@ -393,30 +399,53 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
return await (conn or db).fetch_page(
"SELECT * FROM apipayments",
clause,
values,
filters=filters,
model=Payment,
)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{where}
ORDER BY time DESC
{limit_offset_clause}
""",
tuple(args),
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 [Payment.from_row(row) for row in rows]
return page.data
async def delete_expired_invoices(
@@ -459,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"
@@ -519,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] = []
@@ -604,6 +631,23 @@ async def check_internal(
return row["checking_id"]
async def check_internal_pending(
payment_hash: str, conn: Optional[Connection] = None
) -> bool:
"""Returns False if the internal payment is not pending anymore (and thus paid), otherwise True"""
row = await (conn or db).fetchone(
"""
SELECT pending FROM apipayments
WHERE hash = ? AND amount > 0
""",
(payment_hash,),
)
if not row:
return True
else:
return row["pending"]
# balance_check
# -------------
@@ -690,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,
)
@@ -705,15 +749,19 @@ async def update_admin_settings(data: EditableSettings):
await db.execute("UPDATE settings SET editable_settings = ?", (json.dumps(data),))
async def update_super_user(super_user: str):
async def update_super_user(super_user: str) -> SuperSettings:
await db.execute("UPDATE settings SET super_user = ?", (super_user,))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "updated super_user settings could not be retrieved"
return settings
async def create_admin_settings(super_user: str, new_settings: dict):
sql = "INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
await db.execute(sql, (super_user, json.dumps(new_settings)))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "created admin settings could not be retrieved"
return settings
# db versions
+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)
+3 -2
View File
@@ -107,7 +107,7 @@ async def m002_add_fields_to_apipayments(db):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
prefix = f"#{ext} "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
await db.execute(
@@ -168,7 +168,8 @@ async def m004_ensure_fees_are_always_negative(db):
async def m005_balance_check_balance_notify(db):
"""
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
"""
await db.execute(
+31 -3
View File
@@ -11,9 +11,9 @@ 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
from lnbits.settings import get_wallet_class, settings
from lnbits.wallets.base import PaymentStatus
@@ -75,8 +75,18 @@ class User(BaseModel):
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@classmethod
def is_extension_for_user(cls, ext: str, user: str) -> bool:
if ext not in settings.lnbits_admin_extensions:
return True
if user == settings.super_user:
return True
if user in settings.lnbits_admin_users:
return True
return False
class Payment(BaseModel):
class Payment(FromRowModel):
checking_id: str
pending: bool
amount: int
@@ -204,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
+77 -42
View File
@@ -1,7 +1,7 @@
import asyncio
import json
from io import BytesIO
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple, TypedDict
from urllib.parse import parse_qs, urlparse
import httpx
@@ -13,10 +13,11 @@ from loguru import logger
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.decorators import WalletTypeInfo, require_admin_key
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.helpers import url_for
from lnbits.settings import (
FAKE_WALLET,
EditableSettings,
SuperSettings,
get_wallet_class,
readonly_variables,
send_admin_user_to_saas,
@@ -27,12 +28,14 @@ from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db
from .crud import (
check_internal,
check_internal_pending,
create_account,
create_admin_settings,
create_payment,
create_wallet,
delete_wallet_payment,
get_account,
get_standalone_payment,
get_super_settings,
get_wallet,
get_wallet_payment,
@@ -40,13 +43,9 @@ from .crud import (
update_payment_status,
update_super_user,
)
from .helpers import to_valid_user_id
from .models import Payment
try:
from typing import TypedDict
except ImportError: # pragma: nocover
from typing_extensions import TypedDict
class PaymentFailure(Exception):
pass
@@ -69,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.")
@@ -153,9 +151,25 @@ async def pay_invoice(
extra=extra,
)
# check_internal() returns the checking_id of the invoice we're waiting for
# we check if an internal invoice exists that has already been paid (not pending anymore)
if not await check_internal_pending(invoice.payment_hash, conn=conn):
raise PaymentFailure("Internal invoice already paid.")
# 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(
@@ -169,16 +183,21 @@ async def pay_invoice(
logger.debug(f"creating temporary payment with id {temp_id}")
# create a temporary payment here so we can check if
# the balance is enough in the next step
await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
try:
await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
except Exception as e:
logger.error(f"could not create temporary payment: {e}")
# happens if the same wallet tries to pay an invoice twice
raise PaymentFailure("Could not make payment.")
# do the balance check
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
assert wallet, "Wallet for balancecheck could not be fetched"
if wallet.balance_msat < 0:
logger.debug("balance is too low, deleting temporary payment")
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
@@ -228,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
@@ -326,19 +354,20 @@ async def perform_lnurlauth(
return b
def encode_strict_der(r_int, s_int, order):
def encode_strict_der(r: int, s: int, order: int):
# if s > order/2 verification will fail sometimes
# so we must fix it here (see https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147)
if s_int > order // 2:
s_int = order - s_int
# so we must fix it here see:
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
if s > order // 2:
s = order - s
# now we do the strict DER encoding copied from
# https://github.com/KiriKiri/bip66 (without any checks)
r = int_to_bytes_suitable_der(r_int)
s = int_to_bytes_suitable_der(s_int)
r_temp = int_to_bytes_suitable_der(r)
s_temp = int_to_bytes_suitable_der(s)
r_len = len(r)
s_len = len(s)
r_len = len(r_temp)
s_len = len(s_temp)
sign_len = 6 + r_len + s_len
signature = BytesIO()
@@ -346,16 +375,17 @@ async def perform_lnurlauth(
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(r_temp)
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
signature.write(s_temp)
return signature.getvalue()
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
async with httpx.AsyncClient() as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
r = await client.get(
callback,
params={
@@ -386,7 +416,7 @@ async def check_transaction_status(
return PaymentStatus(None)
if not payment.pending:
# note: before, we still checked the status of the payment again
return PaymentStatus(True)
return PaymentStatus(True, fee_msat=payment.fee)
status: PaymentStatus = await payment.check_status()
return status
@@ -400,24 +430,26 @@ def fee_reserve(amount_msat: int) -> int:
async def update_wallet_balance(wallet_id: str, amount: int):
internal_id = f"internal_{urlsafe_short_hash()}"
payment = await create_payment(
payment_hash, _ = await create_invoice(
wallet_id=wallet_id,
checking_id=internal_id,
payment_request="admin_internal",
payment_hash="admin_internal",
amount=amount * 1000,
amount=amount,
memo="Admin top up",
pending=False,
internal=True,
)
# manually send this for now
from lnbits.tasks import internal_invoice_queue
async with db.connect() as conn:
checking_id = await check_internal(payment_hash, conn=conn)
assert checking_id, "newly created checking_id cannot be retrieved"
await update_payment_status(checking_id=checking_id, pending=False, conn=conn)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(internal_id)
return payment
await internal_invoice_queue.put(checking_id)
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:
@@ -440,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
@@ -455,18 +491,17 @@ 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"])
async def init_admin_settings(super_user: str = None):
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
account = None
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 => {
+55 -26
View File
@@ -146,36 +146,36 @@ new Vue({
paymentsTable: {
columns: [
{
name: 'memo',
name: 'time',
align: 'left',
label: 'Memo',
field: 'memo'
},
{
name: 'date',
align: 'left',
label: 'Date',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: 'Amount (' + LNBITS_DENOMINATION + ')',
field: 'sat',
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'date',
sortable: true
},
{
name: 'fee',
name: 'amount',
align: 'right',
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
}
],
pagination: {
rowsPerPage: 10
rowsPerPage: 10,
page: 1,
sortBy: 'time',
descending: true,
rowsNumber: 10
},
filter: null
filter: null,
loading: false
},
paymentsChart: {
show: false
@@ -466,6 +466,16 @@ new Vue({
return
}
// BIP-21 support
if (this.parse.data.request.toLowerCase().includes('lightning')) {
this.parse.data.request = this.parse.data.request.split('lightning=')[1]
// fail safe to check there's nothing after the lightning= part
if (this.parse.data.request.includes('&')) {
this.parse.data.request = this.parse.data.request.split('&')[0]
}
}
let invoice
try {
invoice = decode(this.parse.data.request)
@@ -510,7 +520,7 @@ new Vue({
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...'
message: this.$t('processing_payment')
})
LNbits.api
@@ -685,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)
+1 -1
View File
@@ -63,7 +63,7 @@
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.name)"
:options="g.extensions.map(e => e.code)"
></q-select>
<br />
</div>
+34 -42
View File
@@ -3,12 +3,14 @@
<div class="row q-col-gutter-md justify-center">
<div class="col q-my-md">
<q-btn
label="Save"
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
<q-tooltip v-if="checkChanges"
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="checkChanges"
@@ -21,13 +23,13 @@
<q-btn
v-if="isSuperUser"
label="Restart server"
:label="$t('restart')"
color="primary"
@click="restartServer"
>
<q-tooltip v-if="needsRestart">
Restart the server for changes to take effect
</q-tooltip>
<q-tooltip v-if="needsRestart"
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="needsRestart"
@@ -40,28 +42,24 @@
<q-btn
v-if="isSuperUser"
label="Topup"
:label="$t('topup')"
color="primary"
@click="topUpDialog.show = true"
>
<q-tooltip> Add funds to a wallet. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
<q-btn
label="Download Database Backup"
flat
@click="downloadBackup"
></q-btn>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-btn
flat
v-if="isSuperUser"
label="Reset to defaults"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip> Delete all settings and reset to defaults. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
</div>
</div>
@@ -74,25 +72,25 @@
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="funding"
label="Funding"
:label="$t('funding')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="users"
label="Users"
:label="$t('users')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="server"
label="Server"
:label="$t('server')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
label="Theme"
:label="$t('theme')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
@@ -113,7 +111,7 @@
<q-dialog v-if="isSuperUser" v-model="topUpDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<p>TopUp a wallet</p>
<p v-text="$t('topup_wallet')"></p>
<div class="row">
<div class="col-12">
@@ -123,7 +121,7 @@
filled
v-model="wallet.id"
label="Wallet ID"
hint="Use the wallet ID to topup any wallet"
:hint="$t('topup_hint')"
></q-input>
<br />
@@ -135,15 +133,25 @@
type="number"
filled
v-model="wallet.amount"
label="Topup amount"
:label="$t('amount')"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
<q-btn
:label="$t('topup')"
color="primary"
@click="topupWallet"
></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
@@ -217,18 +225,6 @@
lnd_rest_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
},
lnd_cert: {
value: null,
label: 'Certificate'
},
lnd_rest_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_rest_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
}
}
],
@@ -251,10 +247,6 @@
value: null,
label: 'Admin Macaroon'
},
lnd_grpc_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
},
lnd_grpc_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
@@ -471,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()
@@ -482,7 +474,7 @@
},
updateSettings() {
let data = _.omit(this.formData, [
'super_user',
'is_super_user',
'lnbits_allowed_funding_sources'
])
LNbits.api
+1 -1
View File
@@ -1,7 +1,7 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:label="$t('api_docs')"
:content-inset-level="0.5"
>
<q-card-section>
+541 -51
View File
@@ -1,48 +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('')"
>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">
You don't have any extensions installed :(
</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>
@@ -62,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 }}
@@ -90,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>
@@ -100,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 %}
+29 -40
View File
@@ -12,24 +12,23 @@
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
>
Press to claim bitcoin
</q-btn>
v-text="$t('press_to_claim')"
></q-btn>
{% else %}
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
filled
dense
v-model="walletName"
label="Name your {{SITE_TITLE}} wallet *"
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
></q-input>
<q-btn
unelevated
color="primary"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
>
:label="$t('add_wallet')"
></q-btn>
</q-form>
{% endif %}
</q-card-section>
@@ -40,27 +39,7 @@
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
<p>
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!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian
solution for others.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p v-text="$t('lnbits_description')"></p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
@@ -69,8 +48,8 @@
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
:label="$t('view_github')"
></q-btn>
<q-btn
outline
color="grey"
@@ -78,8 +57,8 @@
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
:label="$t('donate')"
></q-btn>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION | safe}}</p>
@@ -94,7 +73,7 @@
<q-btn
flat
color="secondary"
label="Runs on"
:label="$t('runs_on')"
class="full-width"
></q-btn>
<div class="row">
@@ -162,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 %}
+172 -135
View File
@@ -37,8 +37,8 @@
>
<q-input
filled
label="{{LNBITS_DENOMINATION}} to credit"
hint="Press Enter to credit account"
:label='$t("credit_label", { denomination: "{{LNBITS_DENOMINATION}}"})'
:hint="$t('credit_hint')"
v-model="scope.value"
dense
autofocus
@@ -63,8 +63,8 @@
color="primary"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
>
:label="$t('paste_request')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -72,8 +72,8 @@
color="primary"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
>
:label="$t('create_invoice')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -81,8 +81,11 @@
color="secondary"
icon="photo_camera"
@click="showCamera"
>scan
<q-tooltip>Use camera to scan an invoice/QR</q-tooltip>
:label="$t('scan')"
>
<q-tooltip
>{% raw %}{{$t('camera_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
@@ -92,12 +95,18 @@
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Transactions</h5>
<h5
class="text-subtitle1 q-my-none"
:v-text="$t('transactions')"
></h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV"
>Export to CSV</q-btn
>
<q-btn
flat
color="grey"
@click="exportCSV"
:label="$t('export_csv')"
></q-btn>
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
<q-tooltip>Check pending</q-tooltip>
</q-btn>-->
@@ -109,35 +118,37 @@
color="grey"
@click="showChart"
>
<q-tooltip>Show chart</q-tooltip>
<q-tooltip
>{% raw %}{{$t('chart_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
:placeholder="$t('search_by_tag_memo_amount')"
class="q-mb-md"
>
</q-input>
<q-table
dense
flat
:data="filteredPayments"
:data="payments"
:row-key="paymentTableRowKey"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
no-data-label="No transactions made yet"
: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
>
@@ -159,7 +170,7 @@
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip>
<q-tooltip>{{$t('pending')}}</q-tooltip>
</q-icon>
</q-td>
<q-td
@@ -180,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 %} {{
@@ -196,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>
@@ -209,7 +218,7 @@
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Invoice waiting to be paid
<span v-text="$t('invoice_waiting')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -232,15 +241,15 @@
outline
color="grey"
@click="copyText(props.row.bolt11)"
>Copy invoice</q-btn
>
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
>Close</q-btn
>
:label="$t('close')"
></q-btn>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
@@ -249,7 +258,7 @@
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
<span v-text="$t('payment_received')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -260,14 +269,14 @@
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
<span v-text="$t('payment_sent')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isOut && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
<span v-text="$t('outgoing_payment_pending')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -304,26 +313,17 @@
<q-expansion-item
group="extras"
icon="crop_free"
label="Drain Funds"
:label="$t('drain_funds')"
>
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping
everything from this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="lightning:{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep
pulling the funds continuously from here after the first
withdraw.
</p>
<p v-text="$('drain_funds_desc')"></p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -333,15 +333,11 @@
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
:label="$t('export_to_phone')"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access.
You can scan it from your phone to open your wallet from
there.
</p>
<p v-text="$t('export_to_phone_desc')"></p>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
@@ -353,7 +349,7 @@
<q-expansion-item
group="extras"
icon="edit"
label="Rename wallet"
:label="$t('rename_wallet')"
>
<q-card>
<q-card-section>
@@ -371,9 +367,9 @@
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_name')"
@click="updateWalletName()"
>Update name</q-btn
>
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -381,20 +377,17 @@
<q-expansion-item
group="extras"
icon="remove_circle"
label="Delete wallet"
:label="$t('delete_wallet')"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<p v-text="$t('delete_wallet_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
:label="$t('delete_wallet')"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -445,7 +438,7 @@
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:label="$('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
reverse-fill-mask
@@ -459,7 +452,7 @@
dense
v-model="receive.unit"
type="text"
label="Unit"
:label="$t('unit')"
:options="receive.units"
></q-select>
<q-input
@@ -467,7 +460,7 @@
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:label="$t('amount') + ' (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
@@ -482,7 +475,7 @@
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
:label="$t('memo')"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
@@ -493,13 +486,17 @@
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
{{$t('withdraw_from')}} {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
@@ -521,12 +518,19 @@
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
<q-btn
outline
color="grey"
@click="copyText(receive.paymentReq)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</q-card>
{% endraw %}
@@ -545,24 +549,42 @@
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong v-text="$t('memo')">:</strong> {{
parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
unelevated
color="primary"
@click="payInvoice"
:label="$t('pay')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('not_enough_funds')"
unelevated
disabled
color="yellow"
text-color="black"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</div>
<div v-else-if="parse.lnurlauth">
@@ -584,9 +606,13 @@
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -657,9 +683,13 @@
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -675,7 +705,7 @@
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
:label="$t('paste_invoice_label')"
>
</q-input>
<div class="row q-mt-lg">
@@ -684,11 +714,15 @@
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
:label="$t('read')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
<div v-else>
@@ -700,8 +734,13 @@
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
<q-btn
:label="$t('cancel')"
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
>
</q-btn>
</div>
</div>
@@ -719,9 +758,13 @@
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
@@ -733,53 +776,47 @@
</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">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
<p>
Login functionality to be released in a future update, for now,
<strong
>make sure you bookmark this page for future access to your
wallet</strong
>!
</p>
<p>
This service is in BETA, and we hold no responsibility for people
losing access to funds. {% if service_fee > 0 %} To encourage you to
run your own LNbits installation, any balance on {% raw %}{{
disclaimerDialog.location.host }}{% endraw %} will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week. {% endif
%}
</p>
<p v-text="$t('disclaimer_dialog')"></p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn
>
:label="$t('copy_wallet_url')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('i_understand')"
></q-btn>
</div>
</q-card>
</q-dialog>
+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"}
+91 -22
View File
@@ -33,11 +33,13 @@ from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import Payment, User, Wallet
from lnbits.core.models import Payment, PaymentFilters, User, Wallet
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
get_key_type,
parse_filters,
require_admin_key,
require_invoice_key,
)
@@ -46,9 +48,10 @@ from lnbits.extension_manager import (
Extension,
ExtensionRelease,
InstallableExtension,
fetch_github_release_config,
get_valid_extensions,
)
from lnbits.helpers import url_for
from lnbits.helpers import generate_filter_params_openapi, url_for
from lnbits.settings import get_wallet_class, settings
from lnbits.utils.exchange_rates import (
currencies,
@@ -64,6 +67,7 @@ from ..crud import (
delete_tinyurl,
get_dbversions,
get_payments,
get_payments_paginated,
get_standalone_payment,
get_tinyurl,
get_tinyurl_by_url,
@@ -114,20 +118,25 @@ async def api_update_wallet(
}
@core_app.get("/api/v1/payments")
@core_app.get(
"/api/v1/payments",
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments(
limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
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,
limit=limit,
offset=offset,
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
)
@@ -135,11 +144,41 @@ async def api_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
filters=filters,
)
@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)
@@ -405,8 +444,7 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
typ, data = await send_queue.get()
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
yield dict(data=jdata, event=typ)
yield dict(data=jdata, event=typ)
except asyncio.CancelledError:
logger.debug(f"removing listener for wallet {uid}")
api_invoice_listeners.pop(uid)
@@ -425,11 +463,12 @@ async def api_payments_sse(
)
# TODO: refactor this route into a public and admin one
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None # type: ignore
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
@@ -499,6 +538,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
assert lnurlauth_key.verifying_key
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
@@ -564,7 +604,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
if k == "text/plain":
params.update(description=v)
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = "data:" + k + "," + v
data_uri = f"data:{k},{v}"
params.update(image=data_uri)
if k in ("text/email", "text/identifier"):
params.update(targetUser=v)
@@ -687,7 +727,7 @@ async def api_auditor():
if not error_message:
delta = node_balance - total_balance
else:
node_balance, delta = None, None
node_balance, delta = 0, 0
return {
"node_balance_msats": int(node_balance),
@@ -697,7 +737,7 @@ async def api_auditor():
}
##################UNIVERSAL WEBSOCKET MANAGER########################
# UNIVERSAL WEBSOCKET MANAGER
@core_app.websocket("/api/v1/ws/{item_id}")
@@ -739,6 +779,12 @@ async def api_install_extension(
raise HTTPException(
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
)
@@ -774,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]
@@ -812,14 +857,17 @@ 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)
)
@core_app.get("/api/v1/extension/{ext_id}/releases")
async def get_extension_releases(ext_id: str, user: User = Depends(check_admin)):
@core_app.get(
"/api/v1/extension/{ext_id}/releases", dependencies=[Depends(check_admin)]
)
async def get_extension_releases(ext_id: str):
try:
extension_releases: List[
ExtensionRelease
@@ -833,7 +881,28 @@ async def get_extension_releases(ext_id: str, user: User = Depends(check_admin))
)
############################TINYURL##################################
@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
@core_app.post("/api/v1/tinyurl")
+38 -35
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,
)
@@ -369,7 +363,7 @@ async def manifest(usr: str):
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
@@ -380,7 +374,7 @@ async def manifest(usr: str):
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
"url": f"/wallet?usr={usr}&wal={wallet.id}",
}
for wallet in user.wallets
],
@@ -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(
+4 -5
View File
@@ -40,19 +40,18 @@ async def api_public_payment_longpolling(payment_hash):
response = None
async def payment_info_receiver(cancel_scope):
async for payment in payment_queue.get():
async def payment_info_receiver():
for payment in await payment_queue.get():
if payment.payment_hash == payment_hash:
nonlocal response
response = {"status": "paid"}
cancel_scope.cancel()
async def timeouter(cancel_scope):
await asyncio.sleep(45)
cancel_scope.cancel()
asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter())
cancel_scope = asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter(cancel_scope))
if response:
return response
+340 -64
View File
@@ -1,12 +1,17 @@
from __future__ import annotations
import asyncio
import datetime
import os
import re
import time
from contextlib import asynccontextmanager
from typing import Optional
from enum import Enum
from sqlite3 import Row
from typing import Any, Generic, List, Literal, Optional, Type, TypeVar
from loguru import logger
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
@@ -17,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>"
@@ -66,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):
@@ -85,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(l) for l 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(
@@ -111,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)
@@ -120,61 +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
import psycopg2
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())
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
psycopg2.extensions.DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.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:
@@ -183,11 +248,13 @@ 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()
try:
async with self.engine.connect() as conn:
async with self.engine.connect() as conn: # type: ignore
async with conn.begin() as txn:
wconn = Connection(conn, txn, self.type, self.name, self.schema)
@@ -217,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)
@@ -224,3 +302,201 @@ class Database(Compat):
@asynccontextmanager
async def reuse_conn(self, conn: Connection):
yield conn
class Operator(Enum):
GT = "gt"
LT = "lt"
EQ = "eq"
NE = "ne"
GE = "ge"
LE = "le"
INCLUDE = "in"
EXCLUDE = "ex"
@property
def as_sql(self):
if self == Operator.EQ:
return "="
elif self == Operator.NE:
return "!="
elif self == Operator.INCLUDE:
return "IN"
elif self == Operator.EXCLUDE:
return "NOT IN"
elif self == Operator.GT:
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 Page(BaseModel, Generic[T]):
data: list[T]
total: int
class Filter(BaseModel, Generic[TFilterModel]):
field: 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[TFilterModel]):
# Key format:
# key[operator]
# e.g. name[eq]
if key.endswith("]"):
split = key[:-1].split("[")
if len(split) != 2:
raise ValueError("Invalid key")
field_names = split[0].split(".")
op = Operator(split[1])
else:
field_names = key.split(".")
op = Operator("eq")
field = field_names[0]
nested = field_names[1:]
if field in model.__fields__:
compare_field = model.__fields__[field]
values = []
for raw_value in raw_values:
# If there is a nested field, pydantic expects a dict, so the raw value is turned into a dict before
# and the converted value is extracted afterwards
for name in reversed(nested):
raw_value = {name: raw_value}
validated, errors = compare_field.validate(raw_value, {}, loc="none")
if errors:
raise ValidationError(errors=[errors], model=model)
for name in nested:
if isinstance(validated, dict):
validated = validated[name]
else:
validated = getattr(validated, name)
values.append(validated)
else:
raise ValueError("Unknown filter field")
return cls(field=field, op=op, nested=nested, values=values, model=model)
@property
def statement(self):
accessor = self.field
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([placeholder] * len(self.values))
stmt = [f"{accessor} {self.op.as_sql} ({placeholders})"]
else:
stmt = [f"{accessor} {self.op.as_sql} {placeholder}"] * len(self.values)
return " OR ".join(stmt)
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 = ""
if self.limit:
stmt += f"LIMIT {self.limit} "
if self.offset:
stmt += f"OFFSET {self.offset}"
return stmt
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 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)
+54 -10
View File
@@ -1,22 +1,27 @@
from http import HTTPStatus
from typing import Literal, Optional, Type
from fastapi import Security, status
from fastapi import Query, Request, Security, status
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic.types import UUID4
from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.db import Filter, Filters, TFilterModel
from lnbits.requestvars import g
from lnbits.settings import settings
# TODO: fix type ignores
class KeyChecker(SecurityBase):
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
@@ -24,13 +29,13 @@ class KeyChecker(SecurityBase):
self._api_key = api_key
if api_key:
key = APIKey(
**{"in": APIKeyIn.query},
**{"in": APIKeyIn.query}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - QUERY",
)
else:
key = APIKey(
**{"in": APIKeyIn.header},
**{"in": APIKeyIn.header}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - HEADER",
)
@@ -70,7 +75,10 @@ class WalletInvoiceKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "invoice"
@@ -86,7 +94,10 @@ class WalletAdminKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "admin"
@@ -174,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:
@@ -200,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:
@@ -266,3 +275,38 @@ async def check_super_user(usr: UUID4) -> User:
detail="User not authorized. No super user privileges.",
)
return user
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,
sortby: Optional[str] = None,
direction: Optional[Literal["asc", "desc"]] = None,
search: Optional[str] = Query(None, description="Text based search"),
):
params = request.query_params
filters = []
for key in params.keys():
try:
filters.append(Filter.parse_query(key, params.getlist(key), model))
except ValueError:
continue
return Filters(
filters=filters,
limit=limit,
offset=offset,
sortby=sortby,
direction=direction,
search=search,
model=model,
)
return dependency
+241 -188
View File
@@ -3,20 +3,170 @@ import json
import os
import shutil
import sys
import urllib.request
import zipfile
from http import HTTPStatus
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Tuple
from urllib import request
import httpx
from fastapi.exceptions import HTTPException
from fastapi import HTTPException
from loguru import logger
from packaging import version
from pydantic import BaseModel
from lnbits.settings import settings
class ExplicitRelease(BaseModel):
id: str
name: str
version: str
archive: str
hash: str
dependencies: List[str] = []
icon: Optional[str]
short_description: 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
organisation: str
repository: str
class Manifest(BaseModel):
featured: List[str] = []
extensions: List["ExplicitRelease"] = []
repos: List["GitHubRelease"] = []
class GitHubRepoRelease(BaseModel):
name: str
tag_name: str
zipball_url: str
html_url: str
class GitHubRepo(BaseModel):
stargazers_count: str
html_url: str
default_branch: str
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):
with request.urlopen(url) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
def file_hash(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
async def fetch_github_repo_info(
org: str, repository: str
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await gihub_api_get(repo_url, error_msg)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
)
error_msg = "Cannot fetch extension releases"
latest_release: Any = await gihub_api_get(lates_release_url, error_msg)
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
error_msg = "Cannot fetch config for extension"
config = await gihub_api_get(config_url, error_msg)
return (
github_repo,
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
async def fetch_manifest(url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await gihub_api_get(url, error_msg)
return Manifest.parse_obj(manifest)
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await gihub_api_get(releases_url, error_msg)
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 = (
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
if settings.lnbits_ext_github_token
else None
)
resp = await client.get(
url,
headers=headers,
)
if resp.status_code != 200:
logger.warning(f"{error_msg} ({url}): {resp.text}")
resp.raise_for_status()
return resp.json()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
tail = "/".join(rest)
return f"https://github.com/{source_repo}/raw/main/{tail}"
class Extension(NamedTuple):
code: str
is_valid: bool
@@ -55,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
@@ -97,12 +247,14 @@ class ExtensionRelease(BaseModel):
version: str
archive: str
source_repo: str
is_github_release = False
hash: Optional[str]
html_url: Optional[str]
description: Optional[str]
details_html: Optional[str] = None
icon: Optional[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
warning: Optional[str] = None
icon: Optional[str] = None
@classmethod
def from_github_release(
@@ -119,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:
@@ -132,52 +302,6 @@ class ExtensionRelease(BaseModel):
return []
class ExplicitRelease(BaseModel):
id: str
name: str
version: str
archive: str
hash: str
dependencies: List[str] = []
icon: Optional[str]
short_description: Optional[str]
html_url: Optional[str]
details: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
class Manifest(BaseModel):
featured: List[str] = []
extensions: List["ExplicitRelease"] = []
repos: List["GitHubRelease"] = []
class GitHubRepoRelease(BaseModel):
name: str
tag_name: str
zipball_url: str
html_url: str
class GitHubRepo(BaseModel):
stargazers_count: str
html_url: str
default_branch: str
class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
class InstallableExtension(BaseModel):
id: str
name: str
@@ -187,8 +311,9 @@ class InstallableExtension(BaseModel):
is_admin_only: bool = False
stars: int = 0
featured = False
latest_release: Optional[ExtensionRelease]
installed_release: Optional[ExtensionRelease]
latest_release: Optional[ExtensionRelease] = None
installed_release: Optional[ExtensionRelease] = None
archive: Optional[str] = None
@property
def hash(self) -> str:
@@ -201,18 +326,18 @@ class InstallableExtension(BaseModel):
return "not-installed"
@property
def zip_path(self) -> str:
extensions_data_dir = os.path.join(settings.lnbits_data_folder, "extensions")
os.makedirs(extensions_data_dir, exist_ok=True)
return os.path.join(extensions_data_dir, f"{self.id}.zip")
def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "extensions")
Path(extensions_data_dir).mkdir(parents=True, exist_ok=True)
return Path(extensions_data_dir, f"{self.id}.zip")
@property
def ext_dir(self) -> str:
return os.path.join("lnbits", "extensions", self.id)
def ext_dir(self) -> Path:
return Path(settings.lnbits_path, "extensions", self.id)
@property
def ext_upgrade_dir(self) -> str:
return os.path.join("lnbits", "upgrades", f"{self.id}-{self.hash}")
def ext_upgrade_dir(self) -> Path:
return Path("lnbits", "upgrades", f"{self.id}-{self.hash}")
@property
def module_name(self) -> str:
@@ -224,21 +349,23 @@ class InstallableExtension(BaseModel):
@property
def has_installed_version(self) -> bool:
if not Path(self.ext_dir).is_dir():
if not self.ext_dir.is_dir():
return False
config_file = os.path.join(self.ext_dir, "config.json")
if not Path(config_file).is_file():
return False
with open(config_file, "r") as json_file:
config_json = json.load(json_file)
return config_json.get("is_installed") is True
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 os.path.isfile(ext_zip_file):
if ext_zip_file.is_file():
os.remove(ext_zip_file)
try:
assert self.installed_release, "installed_release is none."
download_url(self.installed_release.archive, ext_zip_file)
except Exception as ex:
logger.warning(ex)
@@ -250,7 +377,7 @@ class InstallableExtension(BaseModel):
archive_hash = file_hash(ext_zip_file)
if self.installed_release.hash and self.installed_release.hash != archive_hash:
# remove downloaded archive
if os.path.isfile(ext_zip_file):
if ext_zip_file.is_file():
os.remove(ext_zip_file)
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
@@ -258,27 +385,23 @@ class InstallableExtension(BaseModel):
)
def extract_archive(self):
logger.info(f"Extracting extension {self.name}.")
os.makedirs(os.path.join("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)
generated_dir_name = os.listdir(self.ext_upgrade_dir)[0]
os.rename(
os.path.join(self.ext_upgrade_dir, generated_dir_name),
os.path.join(self.ext_upgrade_dir, self.id),
Path(self.ext_upgrade_dir, generated_dir_name),
Path(self.ext_upgrade_dir, self.id),
)
# Pre-packed extensions can be upgraded
# Mark the extension as installed so we know it is not the pre-packed version
with open(
os.path.join(self.ext_upgrade_dir, self.id, "config.json"), "r+"
Path(self.ext_upgrade_dir, self.id, "config.json"), "r+"
) as json_file:
config_json = json.load(json_file)
config_json["is_installed"] = True
json_file.seek(0)
json.dump(config_json, json_file)
json_file.truncate()
self.name = config_json.get("name")
self.short_description = config_json.get("short_description")
@@ -294,10 +417,10 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_dir, True)
shutil.copytree(
os.path.join(self.ext_upgrade_dir, self.id),
os.path.join("lnbits", "extensions", self.id),
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"""
@@ -314,7 +437,7 @@ class InstallableExtension(BaseModel):
def clean_extension_files(self):
# remove downloaded archive
if os.path.isfile(self.zip_path):
if self.zip_path.is_file():
os.remove(self.zip_path)
# remove module from extensions
@@ -322,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"])
@@ -343,8 +475,7 @@ class InstallableExtension(BaseModel):
id=github_release.id,
name=config.name,
short_description=config.short_description,
version="0",
stars=repo.stargazers_count,
stars=int(repo.stargazers_count),
icon=icon_to_github_url(
f"{github_release.organisation}/{github_release.repository}",
config.tile,
@@ -363,7 +494,6 @@ class InstallableExtension(BaseModel):
id=e.id,
name=e.name,
archive=e.archive,
hash=e.hash,
short_description=e.short_description,
icon=e.icon,
dependencies=e.dependencies,
@@ -381,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]
@@ -418,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:
@@ -462,82 +594,3 @@ def get_valid_extensions() -> List[Extension]:
return [
extension for extension in ExtensionManager().extensions if extension.is_valid
]
def download_url(url, save_path):
with urllib.request.urlopen(url) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
def file_hash(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
tail = "/".join(rest)
return f"https://github.com/{source_repo}/raw/main/{tail}"
async def fetch_github_repo_info(
org: str, repository: str
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await gihub_api_get(repo_url, error_msg)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
)
error_msg = "Cannot fetch extension releases"
latest_release: Any = await gihub_api_get(lates_release_url, error_msg)
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
error_msg = "Cannot fetch config for extension"
config = await gihub_api_get(config_url, error_msg)
return (
github_repo,
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
async def fetch_manifest(url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await gihub_api_get(url, error_msg)
return Manifest.parse_obj(manifest)
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await gihub_api_get(releases_url, error_msg)
return [GitHubRepoRelease.parse_obj(r) for r in releases]
async def gihub_api_get(url: str, error_msg: Optional[str]) -> Any:
async with httpx.AsyncClient() as client:
headers = (
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
if settings.lnbits_ext_github_token
else None
)
resp = await client.get(
url,
headers=headers,
)
if resp.status_code != 200:
logger.warning(f"{error_msg} ({url}): {resp.text}")
resp.raise_for_status()
return resp.json()
+62 -76
View File
@@ -1,14 +1,20 @@
import glob
import os
from typing import Any, List, Optional
import json
from pathlib import Path
from typing import Any, List, Optional, Type
import jinja2
import shortuuid # type: ignore
import shortuuid
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
get_model_name_map,
)
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
@@ -16,70 +22,6 @@ def urlsafe_short_hash() -> str:
return shortuuid.uuid()
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".css", prefer_minified)
def sorter(key: str):
if "quasar@" in key:
return 1
if "vue@" in key:
return 2
if "chart.js@" in key:
return 100
return 9
return sorted(paths, key=sorter)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(
os.path.join(settings.lnbits_path, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return sorted(paths)
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, settings.lnbits_path)
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
base = g().base_url if external else ""
url_params = "?"
@@ -89,8 +31,7 @@ def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> s
return url
def template_renderer(additional_folders: List = None) -> Jinja2Templates:
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
folders = ["lnbits/templates", "lnbits/core/templates"]
if additional_folders:
folders.extend(additional_folders)
@@ -107,7 +48,8 @@ def template_renderer(additional_folders: List = None) -> Jinja2Templates:
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
@@ -117,12 +59,15 @@ def template_renderer(additional_folders: List = None) -> Jinja2Templates:
if settings.lnbits_custom_logo:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
if settings.debug:
t.env.globals["VENDORED_JS"] = map(url_for_vendored, get_js_vendored())
t.env.globals["VENDORED_CSS"] = map(url_for_vendored, get_css_vendored())
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
@@ -148,3 +93,44 @@ def get_current_extension_name() -> str:
except:
ext_name = extension_director_name
return ext_name
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
:param keep_optional: If false, all parameters will be optional, otherwise inferred from model
"""
fields = list(model.__fields__.values())
models = get_flat_models_from_fields(fields, set())
namemap = get_model_name_map(models)
params = []
for field in fields:
schema, definitions, _ = field_schema(field, model_name_map=namemap)
# Support nested definition
if "$ref" in schema:
name = schema["$ref"].split("/")[-1]
schema = definitions[name]
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,
"in": "query",
"required": field.required if keep_optional else False,
"schema": schema,
"description": description,
}
params.append(parameter)
return {
"parameters": params,
}
+8 -15
View File
@@ -1,25 +1,18 @@
# Borrowed from the excellent accent-starlette
# https://github.com/accent-starlette/starlette-core/blob/master/starlette_core/templating.py
import typing
from starlette import templating
from jinja2 import BaseLoader, Environment, pass_context
from starlette.datastructures import QueryParams
from starlette.requests import Request
try:
import jinja2
except ImportError: # pragma: nocover
jinja2 = None # type: ignore
from starlette.templating import Jinja2Templates as SuperJinja2Templates
class Jinja2Templates(templating.Jinja2Templates):
def __init__(self, loader: jinja2.BaseLoader) -> None: # pylint: disable=W0231
assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
class Jinja2Templates(SuperJinja2Templates):
def __init__(self, loader: BaseLoader) -> None:
super().__init__("")
self.env = self.get_environment(loader)
def get_environment(self, loader: "jinja2.BaseLoader") -> "jinja2.Environment":
@jinja2.pass_context
def get_environment(self, loader: BaseLoader) -> Environment:
@pass_context
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
request: Request = context["request"]
return request.app.url_path_for(name, **path_params)
@@ -29,7 +22,7 @@ class Jinja2Templates(templating.Jinja2Templates):
values.update(new)
return QueryParams(**values)
env = jinja2.Environment(loader=loader, autoescape=True)
env = Environment(loader=loader, autoescape=True)
env.globals["url_for"] = url_for
env.globals["url_params_update"] = url_params_update
return env
+64 -6
View File
@@ -1,9 +1,11 @@
from http import HTTPStatus
from typing import List, Tuple
from typing import Any, List, Tuple, Union
from urllib.parse import parse_qs
from fastapi.responses import JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.types import ASGIApp, Receive, Scope, Send
from lnbits.helpers import template_renderer
from lnbits.settings import settings
@@ -26,12 +28,21 @@ class InstalledExtensionMiddleware:
else:
_, path_name = path_elements
path_type = None
rest = []
headers = scope.get("headers", [])
# block path for all users if the extension is disabled
if path_name in settings.lnbits_deactivated_extensions:
response = JSONResponse(
status_code=HTTPStatus.NOT_FOUND,
content={"detail": f"Extension '{path_name}' disabled"},
response = self._response_by_accepted_type(
headers, f"Extension '{path_name}' disabled", HTTPStatus.NOT_FOUND
)
await response(scope, receive, send)
return
if not self._user_allowed_to_extension(path_name, scope):
response = self._response_by_accepted_type(
headers, "User not authorized.", HTTPStatus.FORBIDDEN
)
await response(scope, receive, send)
return
@@ -51,6 +62,53 @@ class InstalledExtensionMiddleware:
await self.app(scope, receive, send)
def _user_allowed_to_extension(self, ext_name: str, scope) -> bool:
if ext_name not in settings.lnbits_admin_extensions:
return True
if "query_string" not in scope:
return True
# parse the URL query string into a `dict`
q = parse_qs(scope["query_string"].decode("UTF-8"))
user = q.get("usr", [""])[0]
if not user:
return True
if user == settings.super_user or user in settings.lnbits_admin_users:
return True
return False
def _response_by_accepted_type(
self, headers: List[Any], msg: str, status_code: HTTPStatus
) -> Union[HTMLResponse, JSONResponse]:
"""
Build an HTTP response containing the `msg` as HTTP body and the `status_code` as HTTP code.
If the `accept` HTTP header is present int the request and contains the value of `text/html`
then return an `HTMLResponse`, otherwise return an `JSONResponse`.
"""
accept_header: str = next(
(
h[1].decode("UTF-8")
for h in headers
if len(h) >= 2 and h[0].decode("UTF-8") == "accept"
),
"",
)
if "text/html" in [a for a in accept_header.split(",")]:
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse("error.html", {"request": {}, "err": msg})
.body,
)
return JSONResponse(
status_code=status_code,
content={"detail": msg},
)
class ExtensionsRedirectMiddleware:
# Extensions are allowed to specify redirect paths.
@@ -88,7 +146,7 @@ class ExtensionsRedirectMiddleware:
if "from_path" not in redirect:
return False
header_filters = (
redirect["header_filters"] if "header_filters" in redirect else []
redirect["header_filters"] if "header_filters" in redirect else {}
)
return self._has_common_path(redirect["from_path"], path) and self._has_headers(
header_filters, req_headers
+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
+19 -5
View File
@@ -1,4 +1,5 @@
import importlib
import importlib.metadata
import inspect
import json
import subprocess
@@ -23,6 +24,7 @@ def list_parse_fallback(v):
class LNbitsSettings(BaseSettings):
@classmethod
def validate(cls, val):
if type(val) == str:
val = val.split(",") if val else []
@@ -48,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="")
@@ -102,6 +107,8 @@ class FakeWalletFundingSource(LNbitsSettings):
class LNbitsFundingSource(LNbitsSettings):
lnbits_endpoint: str = Field(default="https://legend.lnbits.com")
lnbits_key: Optional[str] = Field(default=None)
lnbits_admin_key: Optional[str] = Field(default=None)
lnbits_invoice_key: Optional[str] = Field(default=None)
class ClicheFundingSource(LNbitsSettings):
@@ -144,11 +151,14 @@ class LnPayFundingSource(LNbitsSettings):
lnpay_api_endpoint: Optional[str] = Field(default=None)
lnpay_api_key: Optional[str] = Field(default=None)
lnpay_wallet_key: Optional[str] = Field(default=None)
lnpay_admin_key: Optional[str] = Field(default=None)
class OpenNodeFundingSource(LNbitsSettings):
opennode_api_endpoint: Optional[str] = Field(default=None)
opennode_key: Optional[str] = Field(default=None)
opennode_admin_key: Optional[str] = Field(default=None)
opennode_invoice_key: Optional[str] = Field(default=None)
class SparkFundingSource(LNbitsSettings):
@@ -207,8 +217,9 @@ class EditableSettings(
"lnbits_admin_extensions",
pre=True,
)
@classmethod
def validate_editable_settings(cls, val):
return super().validate(cls, val)
return super().validate(val)
@classmethod
def from_dict(cls, d: dict):
@@ -219,12 +230,14 @@ 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="*")
lnbits_path: str = Field(default=".")
lnbits_commit: str = Field(default="unknown")
super_user: str = Field(default="")
version: str = Field(default="0.0.0")
class SaaSSettings(LNbitsSettings):
@@ -269,6 +282,7 @@ class TransientSettings(InstalledExtensionsSettings):
class ReadOnlySettings(
EnvSettings,
ExtensionsInstallSettings,
SaaSSettings,
PersistenceSettings,
SuperUserSettings,
@@ -279,8 +293,9 @@ class ReadOnlySettings(
"lnbits_allowed_funding_sources",
pre=True,
)
@classmethod
def validate_readonly_settings(cls, val):
return super().validate(cls, val)
return super().validate(val)
@classmethod
def readonly_fields(cls):
@@ -299,7 +314,7 @@ class SuperSettings(EditableSettings):
class AdminSettings(EditableSettings):
super_user: bool
is_super_user: bool
lnbits_allowed_funding_sources: Optional[List[str]]
@@ -345,8 +360,6 @@ def send_admin_user_to_saas():
)
############### INIT #################
readonly_variables = ReadOnlySettings.readonly_fields()
transient_variables = TransientSettings.readonly_fields()
@@ -366,6 +379,7 @@ try:
except:
settings.lnbits_commit = "docker"
settings.version = importlib.metadata.version("lnbits")
# printing environment variable for debugging
if not settings.lnbits_admin_ui:
+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'
}
+123
View File
@@ -0,0 +1,123 @@
window.localisation.de = {
server: 'Server',
theme: 'Theme',
funding: 'Funding',
users: 'Benutzer',
unit: 'Einheit',
restart: 'Server neu starten',
save: 'Speichern',
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',
add_funds_tooltip: 'Füge Geld zu einer Wallet hinzu.',
reset_defaults: 'Zurücksetzen',
reset_defaults_tooltip:
'Alle Einstellungen auf die Standardeinstellungen zurücksetzen.',
download_backup: 'Datenbank-Backup herunterladen',
name_your_wallet: 'Vergib deiner %{name} Wallet einen Namen',
paste_invoice_label:
'Füge eine Rechnung, Zahlungsanforderung oder LNURL ein *',
lnbits_description:
'Einfach zu installieren und kompakt, LNbits kann auf jeder Funding-Quelle im Lightning Netzwerk aufsetzen. Derzeit unterstützt: LND, Core Lightning, OpenNode, LNPay und sogar LNbits selbst! Du kannst LNbits für dich selbst betreiben oder anderen die Verwaltung durch dich anbieten. Jede Wallet hat ihre eigenen API-Schlüssel und die Anzahl der Wallets ist unbegrenzt. Die Möglichkeit, Gelder auf verschiedene Accounts mit unterschiedlicher Logik aufteilen zu können macht LNbits zu einem nützlichen Werkzeug für deine Buchhaltung - aber auch als Entwicklungswerkzeug. Erweiterungen bereichern LNbits Accounts um zusätzliche Funktionalität, so dass du mit einer Reihe von neuartigen Technologien auf dem Lightning-Netzwerk experimentieren kannst. Wir haben es so einfach wie möglich gemacht, Erweiterungen zu entwickeln, und als freies und Open-Source-Projekt möchten wir Menschen ermutigen, sich selbst hieran zu versuchen und gemeinsam mit uns neue Funktionalitäten zu entwickeln.',
export_to_phone: 'Auf dem Telefon öffnen',
export_to_phone_desc:
'Dieser QR-Code beinhaltet vollständige Rechte auf deine Wallet. Du kannst den QR-Code mit Deinem Telefon scannen, um deine Wallet dort zu öffnen.',
wallets: 'Wallets',
add_wallet: 'Wallet hinzufügen',
delete_wallet: 'Wallet löschen',
delete_wallet_desc:
'Die Wallet wird gelöscht, die hierin beinhalteten Daten hierin oder innerhalb einer Erweiterung sind UNWIEDERBRINGLICH.',
rename_wallet: 'Wallet umbenennen',
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:
'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: 'Klicke Enter, um das Konto zu belasten',
credit_label: '%{denomination} zu belasten',
paste_request: 'Anfrage einfügen',
create_invoice: 'Rechnung erstellen',
camera_tooltip:
'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',
copy_invoice: 'Rechnung kopieren',
close: 'Schließen',
cancel: 'Stornieren',
scan: 'Scannen',
read: 'Lesen',
pay: 'Zahlen',
memo: 'Memo',
date: 'Datum',
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 wartend auf Zahlung',
payment_received: 'Zahlung erhalten',
payment_sent: 'Zahlung gesendet',
outgoing_payment_pending: 'Ausgehende Zahlung wartend',
drain_funds: 'Sats abziehen',
drain_funds_desc:
'LNURL-withdraw QR-Code, der das Abziehen aller Geldmittel aus dieser Wallet erlaubt. Teile ihn mit niemandem! Kompatibel mit balanceCheck und balanceNotify, so dass 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. 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: '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: 'Beleg'
}
+116
View File
@@ -0,0 +1,116 @@
window.localisation.en = {
server: 'Server',
theme: 'Theme',
funding: 'Funding',
users: 'Users',
restart: 'Restart server',
save: 'Save',
save_tooltip: 'Save your changes',
topup: 'Topup',
topup_wallet: 'Topup a wallet',
topup_hint: 'Use the wallet ID to topup any wallet',
restart_tooltip: 'Restart the server for changes to take effect',
add_funds_tooltip: 'Add funds to a wallet.',
reset_defaults: 'Reset to defaults',
reset_defaults_tooltip: 'Delete all settings and reset to defaults.',
download_backup: 'Download database backup',
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, 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.',
wallets: 'Wallets',
add_wallet: 'Add a new wallet',
delete_wallet: 'Delete wallet',
delete_wallet_desc:
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
rename_wallet: 'Rename wallet',
update_name: 'Update name',
press_to_claim: 'Press to claim bitcoin',
donate: 'Donate',
view_github: 'View on GitHub',
voidwallet_active: 'VoidWallet is active! Payments disabled',
use_with_caution: 'USE WITH CAUTION - %{name} wallet is still in BETA',
toggle_darkmode: 'Toggle Dark Mode',
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',
paste_request: 'Paste Request',
create_invoice: 'Create Invoice',
camera_tooltip: 'Use camera to scan an invoice/QR',
export_csv: 'Export to CSV',
transactions: 'Transactions',
chart_tooltip: 'Show chart',
pending: 'Pending',
copy_invoice: 'Copy invoice',
close: 'Close',
cancel: 'Cancel',
scan: 'Scan',
read: 'Read',
pay: 'Pay',
memo: 'Memo',
date: 'Date',
processing_payment: 'Processing payment...',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received',
payment_sent: 'Payment Sent',
outgoing_payment_pending: 'Outgoing payment pending',
drain_funds: 'Drain Funds',
drain_funds_desc:
'This is an LNURL-withdraw QR code for slurping everything from this wallet. Do not share with anyone. It is compatible with balanceCheck and balanceNotify so your wallet may keep pulling the funds continuously from here after the first withdraw.',
i_understand: 'I understand',
copy_wallet_url: 'Copy wallet URL',
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_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',
unit: 'Unit',
description: 'Description',
expiry: 'Expiry',
webhook: 'Webhook',
payment_proof: 'Payment Proof'
}
+119
View File
@@ -0,0 +1,119 @@
window.localisation.es = {
server: 'Servidor',
theme: 'Tema',
funding: 'Financiación',
unit: 'Unidad',
users: 'Usuarios',
restart: 'Reiniciar el servidor',
save: 'Guardar',
save_tooltip: 'Guardar cambios',
topup: 'Recargar',
topup_wallet: 'Recargar billetera',
topup_hint: 'Utilice el ID de billetera para recargar cualquier billetera',
restart_tooltip: 'Reinicie el servidor para aplicar los cambios',
add_funds_tooltip: 'Agregue fondos a una billetera.',
reset_defaults: 'Restablecer',
reset_defaults_tooltip:
'Borrar todas las configuraciones y restablecer a los valores predeterminados.',
download_backup: 'Descargar copia de seguridad de la base de datos',
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, 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í.',
wallets: 'Billeteras',
add_wallet: 'Agregar nueva billetera',
delete_wallet: 'Eliminar billetera',
delete_wallet_desc:
'Esta billetera completa se eliminará, los fondos son IRREVERSIBLES.',
rename_wallet: 'Cambiar el nombre de la billetera',
update_name: 'Actualizar nombre',
press_to_claim: 'Presione para reclamar Bitcoin',
donate: 'Donar',
view_github: 'Ver en GitHub',
voidwallet_active: '¡VoidWallet está activo! Pagos desactivados',
use_with_caution: 'USAR CON CUIDADO - %{name} Wallet aún está en BETA',
toggle_darkmode: 'Cambiar modo oscuro',
view_swagger_docs: 'Ver documentos de API de LNbits Swagger',
api_docs: 'Documentos de API',
commit_version: 'Versión de compromiso',
runs_on: 'Corre en',
credit_hint: 'Presione Enter para cargar la cuenta',
credit_label: 'Cargar %{denomination}',
paste_request: 'Pegar solicitud',
create_invoice: 'Crear factura',
camera_tooltip: 'Utilice la cámara para escanear una factura / código QR',
export_csv: 'Exportar a CSV',
transactions: 'Transacciones',
chart_tooltip: 'Mostrar gráfico',
pending: 'Pendiente',
copy_invoice: 'Copiar factura',
close: 'Cerrar',
cancel: 'Cancelar',
scan: 'Escanear',
read: 'Leer',
pay: 'Pagar',
memo: 'Memo',
date: 'Fecha',
processing_payment: 'Procesando pago ...',
not_enough_funds: '¡No hay suficientes fondos!',
search_by_tag_memo_amount: 'Buscar por etiqueta, memo, cantidad',
invoice_waiting: 'Factura esperando pago',
payment_received: 'Pago recibido',
payment_sent: 'Pago enviado',
outgoing_payment_pending: 'Pago saliente pendiente',
drain_funds: 'Drenar fondos',
drain_funds_desc:
'Este es un código QR LNURL-withdraw para drenar todos los fondos de esta billetera. No lo comparta con nadie. Es compatible con balanceCheck y balanceNotify, por lo que su billetera puede continuar drenando los fondos de aquí después del primer drenaje.',
i_understand: 'Lo entiendo',
copy_wallet_url: 'Copiar URL de billetera',
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_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',
description: 'Descripción',
expiry: 'Expiración',
webhook: 'Webhook',
payment_proof: 'Prueba de pago'
}
+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'
}
+1
View File
@@ -0,0 +1 @@
window.localisation = {}
+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'
}
+115
View File
@@ -0,0 +1,115 @@
window.localisation.jp = {
server: 'サーバー',
theme: 'テーマ',
funding: '資金調達',
users: 'ユーザー',
restart: 'サーバーを再起動する',
save: '保存',
unit: '単位',
save_tooltip: '変更を保存する',
topup: 'トップアップ',
topup_wallet: 'ウォレットをトップアップする',
topup_hint: 'ウォレットIDを使用して、任意のウォレットをトップアップできます',
restart_tooltip: 'サーバーを再起動して変更を適用します',
add_funds_tooltip: 'ウォレットに資金を追加します。',
reset_defaults: 'リセット',
reset_defaults_tooltip: 'すべての設定を削除してデフォルトに戻します。',
download_backup: 'データベースのバックアップをダウンロードする',
name_your_wallet: 'あなたのウォレットの名前 %{name}',
paste_invoice_label: '請求書を貼り付けてください',
lnbits_description:
'簡単にインストールでき、軽量で、LNbitsは現在LND、Core Lightning、OpenNode、LNPay、さらにLNbits自身で動作する任意のLightningネットワークの資金源で実行できます! LNbitsを自分で実行することも、他の人に優れたソリューションを提供することもできます。各ウォレットには独自のAPIキーがあり、作成できるウォレットの数に制限はありません。資金を分割する機能は、LNbitsを資金管理ツールとして使用したり、開発ツールとして使用したりするための便利なツールです。拡張機能は、LNbitsに追加の機能を追加します。そのため、LNbitsは最先端の技術をネットワークLightningで試すことができます。拡張機能を開発するのは簡単で、無料でオープンソースのプロジェクトであるため、人々が自分で開発し、自分の貢献を送信することを奨励しています。',
export_to_phone: '電話にエクスポート',
export_to_phone_desc:
'ウォレットを電話にエクスポートすると、ウォレットを削除する前にウォレットを復元できます。ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
wallets: 'ウォレット',
add_wallet: 'ウォレットを追加',
delete_wallet: 'ウォレットを削除',
delete_wallet_desc:
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。',
rename_wallet: 'ウォレットの名前を変更',
update_name: '名前を更新',
press_to_claim: 'クレームするには押してください',
donate: '寄付',
voidwallet_active: 'Voidwalletアクティブ',
use_with_caution: '注意して使用してください',
toggle_dark_mode: 'ダークモードを切り替える',
view_swagger_docs: 'Swaggerドキュメントを表示',
api_docs: 'APIドキュメント',
commit_version: 'コミットバージョン',
runs_on: 'で実行',
credit_hint:
'クレジットカードを使用して資金を追加するには、LNbitsを使用してください。',
credit_label: 'クレジットカード',
paste_request: 'リクエストを貼り付ける',
create_invoice: '請求書を作成する',
camera_tooltip: 'QRコードを読み取る',
export_csv: 'CSVでエクスポート',
transactions: 'トランザクション',
chart_tooltip: 'チャートを表示するには、グラフの上にカーソルを合わせます',
pending: '保留中',
copy_invoice: '請求書をコピー',
close: '閉じる',
cancel: 'キャンセル',
scan: 'スキャン',
read: '読む',
pay: '支払う',
memo: 'メモ',
date: '日付',
processing_payment: '支払い処理中',
not_enough_funds: '資金が不足しています',
search_by_tag_memo_amount: 'タグ、メモ、金額で検索',
invoice_waiting: '請求書を待っています',
payment_received: 'お支払いありがとうございます',
payment_sent: '支払いが完了しました',
outgoing_payment_pending: '支払い保留中',
drain_funds: '資金を排出する',
drain_funds_desc: 'ウォレットの残高をすべて他のウォレットに送金します',
i_understand: '理解した',
copy_wallet_url: 'ウォレットURLをコピー',
disclaimer_dialog:
'ウォレットを削除すると、ウォレットの秘密鍵が削除され、ウォレットを復元することはできません。ウォレットを削除する前に、ウォレットをエクスポートしてください。',
no_transactions: 'トランザクションはありません',
manage_server: 'サーバーを管理する',
extensions: '拡張機能',
no_extensions: '拡張機能はありません',
created: '作成済み',
search_extensions: '検索拡張機能',
warning: '警告',
manage: '管理',
repository: 'リポジトリ',
confirm_continue: '続行してもよろしいですか?',
manage_extension_details: '拡張機能のインストール/アンインストール',
install: 'インストール',
uninstall: 'アンインストール',
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: '量',
description: '説明',
expiry: '有効期限',
webhook: 'ウェブフック',
payment_proof: '支払い証明'
}
+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

+31 -4
View File
@@ -1,6 +1,14 @@
/* globals crypto, moment, Vue, axios, Quasar, _ */
Vue.use(VueI18n)
window.LOCALE = 'en'
window.i18n = new VueI18n({
locale: window.LOCALE,
fallbackLocale: window.LOCALE,
messages: window.localisation
})
window.EventHub = new Vue()
window.LNbits = {
api: {
@@ -59,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(
@@ -177,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,
@@ -320,6 +333,7 @@ window.LNbits = {
}
window.windowMixin = {
i18n: window.i18n,
data: function () {
return {
g: {
@@ -329,12 +343,17 @@ window.windowMixin = {
user: null,
wallet: null,
payments: [],
allowedThemes: null
allowedThemes: null,
langs: []
}
}
},
methods: {
changeLanguage: function (newValue) {
window.i18n.locale = newValue
this.$q.localStorage.set('lnbits.lang', newValue)
},
changeColor: function (newValue) {
document.body.setAttribute('data-theme', newValue)
this.$q.localStorage.set('lnbits.theme', newValue)
@@ -364,6 +383,14 @@ window.windowMixin = {
}
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
let locale = this.$q.localStorage.getItem('lnbits.lang')
if (locale) {
window.LOCALE = locale
window.i18n.locale = locale
}
this.g.langs = window.langs ?? []
addEventListener('offline', event => {
this.g.offline = true
})
+73 -75
View File
@@ -28,7 +28,7 @@ Vue.component('lnbits-wallet-list', {
},
template: `
<q-list v-if="user && user.wallets.length" dense class="lnbits-drawer__q-list">
<q-item-label header>Wallets</q-item-label>
<q-item-label header v-text="$t('wallets')"></q-item-label>
<q-item v-for="wallet in wallets" :key="wallet.id"
clickable
:active="activeWallet && activeWallet.id === wallet.id"
@@ -56,7 +56,7 @@ Vue.component('lnbits-wallet-list', {
<q-icon :name="(showForm) ? 'remove' : 'add'" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Add a wallet</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('add_wallet')"></q-item-label>
</q-item-section>
</q-item>
<q-item v-if="showForm">
@@ -112,7 +112,7 @@ Vue.component('lnbits-extension-list', {
},
template: `
<q-list v-if="user" dense class="lnbits-drawer__q-list">
<q-item-label header>Extensions</q-item-label>
<q-item-label header v-text="$t('extensions')"></q-item-label>
<q-item v-for="extension in userExtensions" :key="extension.code"
clickable
:active="extension.isActive"
@@ -137,15 +137,7 @@ Vue.component('lnbits-extension-list', {
<q-icon name="clear_all" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">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">Manage Extensions</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('extensions')"></q-item-label>
</q-item-section>
</q-item>
</q-list>
@@ -199,7 +191,7 @@ Vue.component('lnbits-admin-ui', {
<q-icon name="admin_panel_settings" color="grey-5" size="md"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label lines="1" class="text-caption">Manage Server</q-item-label>
<q-item-label lines="1" class="text-caption" v-text="$t('manage_server')"></q-item-label>
</q-item-section>
</q-item>
</q-list>
@@ -214,74 +206,80 @@ Vue.component('lnbits-admin-ui', {
Vue.component('lnbits-payment-details', {
props: ['payment'],
mixins: [windowMixin],
data: function () {
return {
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
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>Created</b>:</div>
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
</div>
<div class="row">
<div class="col-3"><b>Expiry</b>:</div>
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
</div>
<div class="row">
<div class="col-3"><b>Description</b>:</div>
<div class="col-9">{{ payment.memo }}</div>
</div>
<div class="row">
<div class="col-3"><b>Amount</b>:</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Fee</b>:</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Payment hash</b>:</div>
<div class="col-9 text-wrap mono">{{ payment.payment_hash }}</div>
</div>
<div class="row" v-if="payment.webhook">
<div class="col-3"><b>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>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"
]
}
+20776
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
/*
* DOM element rendering detection
* https://davidwalsh.name/detect-node-insertion
*/
@keyframes chartjs-render-animation {
from { opacity: 0.99; }
to { opacity: 1; }
}
.chartjs-render-monitor {
animation: chartjs-render-animation 0.001s;
}
/*
* DOM element resizing detection
* https://github.com/marcj/css-element-queries
*/
.chartjs-size-monitor,
.chartjs-size-monitor-expand,
.chartjs-size-monitor-shrink {
position: absolute;
direction: ltr;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
visibility: hidden;
z-index: -1;
}
.chartjs-size-monitor-expand > div {
position: absolute;
width: 1000000px;
height: 1000000px;
left: 0;
top: 0;
}
.chartjs-size-monitor-shrink > div {
position: absolute;
width: 200%;
height: 200%;
left: 0;
top: 0;
}
File diff suppressed because it is too large Load Diff
+2964
View File
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 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
File diff suppressed because one or more lines are too long
+5685
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+11680
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2042
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+2312
View File
File diff suppressed because it is too large Load Diff
@@ -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%}
File diff suppressed because one or more lines are too long
+5509
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More