Compare commits

...
453 Commits
Author SHA1 Message Date
ArcandGitHub 7783f34998 bug: wrong pending status (#2352) 2024-03-24 13:58:15 +01:00
dni ⚡ 7dcb2fcdd5 chore: release 0.12.4 2024-03-24 13:44:47 +01:00
dni ⚡andGitHub 033a57cad4 chore: update to 0.12.3 (#2344) 2024-03-22 13:03:00 +01:00
dni ⚡andGitHub 299228b7b5 feat: reduce initial requests on wallet page (#2335)
* feat: reduce initial request on wallet page
- refactor allowed_currencies into function to use in generic and api.
- remove currencies request in frontend move it to generic
- dont request balance on first payments fetch
2024-03-22 12:59:49 +01:00
dni ⚡andGitHub 5b022e2ef3 feat: make route hints configurable for lndrest (#2304)
* feat: make route hints configurable for lndrest
boolean if route hints for private channels will be included from `LndRest` inside bolt11 invoice.
2024-03-22 12:45:39 +01:00
Pavol RusnakandGitHub c8818f5774 remove CACHE_VERSION from service-worker.js (#2342)
use server_startup_time as the version
(the same trick we use for static files)
2024-03-22 12:33:42 +01:00
dni ⚡andGitHub 1398857688 refactor: move registering of tasks into app.py (#2290)
* refactor: move registering of tasks into `app.py`

let the app have the responsibility to create the tasks not a helper function inside tasks.py.
makes it way easier to follow on how those background tasks are ran.
2024-03-22 12:29:05 +01:00
Gonçalo ValérioandGitHub 2a1505bc0d bug: Fix regression. Admin-users and allowed-users can now be removed through the web interface (#2331)
* Fix regression. Admin-users and allowed-users can now be removed through the web interface
* remove raw usage from user tab on server admin
2024-03-22 07:26:25 +01:00
dni ⚡andGitHub 4bba6ecc82 hotfix: qrcode broke wallet.html (#2340)
introduced by 43a79744
2024-03-21 17:39:12 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
522c4df44c chore(deps): bump follow-redirects from 1.15.5 to 1.15.6 (#2326)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-21 16:43:48 +01:00
dni ⚡andGitHub c03b81d2ea refactor: tiny url to use require decorators and wallet.id (#2338)
- also use `wallet.wallet.id` as key instead of `wallet.wallet.inkey`
2024-03-21 13:32:55 +01:00
arbadacarbaandPavol Rusnak 43a797444a recover user for export-to-phone 2024-03-21 13:14:28 +01:00
dni ⚡andPavol Rusnak d0cb961b49 bug: wallet page fetchbalance ran twice
there is a listener on payments which executes fetchbalance automatically if payments change. line 822
2024-03-21 13:13:33 +01:00
ArcandGitHub d208e68885 feat: added home button on error page (#2330)
* Added redirect on error page if user id not present as users keep getting stuck
2024-03-21 12:24:38 +01:00
dni ⚡andGitHub 8dcb53aea0 chore: keychecker todo remove type ignores (#2337)
fixes a comment `TODO`
2024-03-21 11:36:01 +01:00
dni ⚡andGitHub 241b286e21 chore: update ruff and black in precommit config (#2336)
ran `poetry lock --no-update`
2024-03-21 11:05:01 +01:00
dni ⚡andGitHub 17839f6a25 feat: add database debug env var (#2333)
* feat: add database debug env var
makes it visible what db actions happen
2024-03-21 09:59:17 +01:00
dni ⚡andGitHub e4d3faefa0 feat: create_unique_task helper (#2292)
this is especially useful for extension.

with this an extension can only call `create_permanent_unique_task("my-ext", task)` and it will restarted when it exists and cancels when lnbits is stopped
2024-03-18 13:02:04 +00:00
116ca7f011 Remove all {% raw %} usage (#2318)
* Remove raw from templates 
* Remove v-text from q-tooltip
* extension buttons
* Move more to spans
* Reformatted

---------

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2024-03-15 11:10:52 +01:00
Vlad StanandGitHub d44339b018 fix: pending property for PaymentStatus (#2324)
* fix: pending property for PaymentStatus

* fix: invoice status

* fix: check pending status from the payment details

* refactor: make condition more explicit
2024-03-14 14:38:10 +02:00
Vlad StanandGitHub 352fd23c0b refactor: dev-friendly payment status name (#2322)
* refactor: dev-friendly payment status name
* refactor: change `PaymentStatus(True, ...)` to `PaymentSuccessStatus(...)`
2024-03-13 16:17:33 +01:00
Vlad StanandGitHub 65b8868c36 fix: enforce order of payments (#2313)
* fix: enforce order of payments

* fix: do not return wallet by key if the wallet is deleted
2024-03-12 15:31:40 +02:00
dni ⚡andGitHub 54dec171f9 fix: raise failed request to not run mark_webhook_sent (#2289)
* fix: webhook sent raise a failed request

is not raised even failed webhook would be marked is sent

* add warn log

* fix error

* improve exceptions

* fixup!

* ConnectError is already captured by RequestError

https://www.python-httpx.org/exceptions/

* log exception
2024-03-12 15:12:24 +02:00
Vlad StanandGitHub 5b4398911a fix: check if key present in settings before updating (#2306) 2024-03-12 13:03:11 +00:00
4c0bd132b1 bug: cln paymentstate check on error (#2151)
* CLN: paymentstate check on error

---------

Co-authored-by: dni  <office@dnilabs.com>
2024-03-12 13:56:36 +01:00
7ce4eddb0e feat: add group_by to fetch_page (#2140)
---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-03-12 13:55:38 +01:00
callebtcandGitHub 14519135d8 Fix: LNbits backend payent state (#2187) 2024-03-12 13:43:41 +01:00
dni ⚡andGitHub 60839481ad feat: add tool preimage generator (#2321)
`poetry run python tools/preimage.py`
2024-03-12 11:46:46 +01:00
dni ⚡andGitHub 3ef1941fc0 test: fix TestModel warning (#2317)
PytestCollectionWarning: cannot collect test class 'TestModel' because it has a __init__ constructor (from: tests/core/test_helpers_query.py)
2024-03-12 11:32:10 +01:00
dni ⚡andGitHub 16cb1a8026 fix: lnbits funding source (#2319)
fix a bug where payments are deleted
2024-03-12 11:28:42 +01:00
dni ⚡andGitHub e3b9bd6a70 fix: update pre-commit ruff (#2316)
wasnt working after latest ruff update
2024-03-12 08:47:13 +01:00
Vlad StanandGitHub a1ea04acf8 Jmeter take 3 (#2284)
* chore: add tests

* chore: add reference to jmter job

* chore: add jmeter job

* chore: touch tests

* chorE: disable other workflows

* chore: do not set `LNBITS_ADMIN_UI: true`

* chore: extra log file

* chore: rollback

* chore: testing stuff

* feat: top-up wallet

* chore: clean-up usr

* chore: clean-up user query param

* chore: remove hardcoded usr

* chore: clean-up

* chore: clean-up

* chore: different improvements

* chore: clean-up

* chore: remove hardcoded values

* refactor: extract `init-server` fragment

* chore: move fragment

* feat: use test frgment

* chore: clean-up hardcoded values

* chore: clean-up

* fix: get lnurl working

* chore: re-arange elements

* chore: re-arange elements

* fix: add `lnurlp, withdraw` extensions

* chore: rename file

* fix: login with user id

* refactor: extract init account

* fix: tips wallet

* fix: assertion

* refactor: use fragments; group loops

* chore: re-enable the other CI jobs

* fix: separate wallet for receive

* feat: check receive wallet balance

* feat: increase wait time
2024-03-08 16:40:23 +02:00
3e341a3555 update black to 24.2.0 and ruff to 0.3.0 (#2307)
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2024-03-07 09:55:37 +00:00
Vlad StanandGitHub cb5c9b03bf Fix login logout on safari (#2309)
* fix: relax security level for public cookies

* fix: corner case for logout
2024-03-07 09:22:42 +00:00
dni ⚡andGitHub 884a1b9d6f test: lint tests and tools aswell (#2296)
* test: lint `tests` and `tools` aswell

more linting :)

* fix linting issues in tests and tools

* fixup!

* how is this working?
2024-02-27 13:30:52 +00:00
Vlad StanandGitHub e8aa498683 feat: add get endpoint for user wallets (#2299)
* feat: add get endpoint for user wallets

* feat: only expose relevant fields

* refactor: extract `BaseWallet`

* doc: add open api doc
2024-02-27 10:08:10 +02:00
Pavol Rusnak 1b7efd854a chore: make bundle 2024-02-23 16:11:27 -03:00
Pavol Rusnak 8b32c3dcb6 remove webhook_listener
following commit e419c74ebb
2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 0821b28eac rename self.auth to headers 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 4c71d5ac42 format file 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak a8e0b5a5ac use bolt11.decoder from lnbits instead of zbd remote 2024-02-23 16:11:27 -03:00
BitkarrotandPavol Rusnak 9b9014a042 Update zbd.py
set timeout on pay_invoice to 40
2024-02-23 16:11:27 -03:00
Pavol Rusnak 96b42b1784 add ZBD to homepage 2024-02-23 16:11:27 -03:00
BloggingBitcoinandPavol Rusnak 26d854ebe8 added ZBD logos 2024-02-23 16:11:27 -03:00
Pavol Rusnak ed36d01b40 fix typing issues 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak 646556a457 add zbd to translation files 2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak d20a35eddc add zbd to settings
update api methods

add zbd as funding source to settings and js

fix statuses

cast str into int for status method

fix outbound payment hash issue

restore create_invoice

remove print stmts
2024-02-23 16:11:27 -03:00
bitkarrotandPavol Rusnak d69946db8a initial scaffolding for zbd wallet 2024-02-23 16:11:27 -03:00
dni ⚡andPavol Rusnak ddab4075d2 fix: raise proper exception in services pay_invoice
also values where checked twice
2024-02-23 00:39:00 -03:00
d6c8ad1d0d Paid extensions (#2229)
* fix: download archive file `async`

* feat: add `pay_link` property

* feat: basic install using internal wallet for payment

* fix: pop-up issues

* chore: refactor

* feat: detect paid extensions

* fix: payment check

* feat: small stuff

* feat: show external invoice

* fix: regression for extension install

* feat: store previos successful payments

* refactor: simplify, almost works

* chore: gugu gaga

* fix: pay and install

* fix: do not pay invoice on the back-end

* chore: code clean-up

* feat: basic websocker listener

* feat: use websocket to watch for invoice payment

* feat: remember hanging invoices

* refactor: extract `localStorage` methods

* chore: code format

* chore: code clean-up after test

* feat: remember previous payment_hashes

* chore: code format

* refactor: rename `ExtensionPaymentInfo` to `ReleasePaymentInfo`

* refactor: method rename

* fix: release version matters now

* chore: code format

* refactor: method rename

* refactor: extract method `_restore_payment_info`

* refactor: extract method

* chore: rollback `CACHE_VERSION`

* chore: code format

* feat: i18n

* chore: update bundle

* refactor: public method name

* chore: code format

* fix: websocket connection

* Update installation.md (#2259)

* Update installation.md (#2260)

* fix: try to fix `openapi` error

* chore: bundle

* chore:bundle

---------

Co-authored-by: benarc <ben@arc.wales>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2024-02-22 13:16:41 +00:00
dni ⚡andPavol Rusnak 54c6faa4b6 refactor: move mark_webhook_sent into crud.py
database functions should be in crud
2024-02-21 11:14:04 -03:00
dni ⚡andPavol Rusnak c88f05c5c0 refactor: tasks should be run with internal wrapper
listeners used did not use the helper function
2024-02-21 10:56:31 -03:00
dni ⚡andPavol Rusnak e419c74ebb refactor: remove unused webhook handler for wallets
it was only used in lnpay, opennode and alby which all had it disabled anayways with a pretty old TODO, so i guess the feature is disabled for a y ear now and nobody used it, so i just removed it
2024-02-21 10:55:57 -03:00
dni ⚡andGitHub b6dc66b070 chore: remove dead code send_push_promise (#2285)
dead code removal :)
2024-02-21 14:24:06 +02:00
dni ⚡andGitHub 7b4d9bf5db chore: update to 0.12.2 (#2282)
update!!!
2024-02-21 11:09:43 +01:00
Vlad StanandGitHub c51e7351e8 fix: extension stop background work (#2281)
* feat: add helper methods

* fix: do not try to stop background work on first install

* fix: first stop via function call then try REST API

* fix: `make check`

* fix: prepare for `{ext_id}_stop`
2024-02-21 12:08:37 +02:00
fe12eccc42 Updated new wallet warning modal text to clarify importance of backing up login creds (#2273)
* Updated new wallet warning modal text to clarify importance of backing up creds


Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-02-20 14:36:39 +02:00
dni ⚡andGitHub 17b7826753 feat: unique invoice listeners for extensions (#2250)
changes the `register_invoice_listener` to use a non unique key so you its not possible to have a listeners running twice and throw a warning when trying to.
2024-02-20 14:03:29 +02:00
14ae6d8b1a feat: add node url to api keys & docs (#2283)
* feat: add node url to api keys & docs

closes #2277

* add finnish to tools/i18n-ai-tool.py

* regenerate localization with i18n-ai-tool

* chore: make bundle

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-02-20 13:32:49 +02:00
dni ⚡andGitHub 7b559991c7 fix: docker image invoice listener were not stopping (#2278)
because it could not resolve localhost
2024-02-16 15:24:50 +02:00
dni ⚡andGitHub decd4cdf0a chore: remove listener dict (#2279) 2024-02-16 14:47:52 +02:00
dni ⚡andGitHub dc7a6551c5 chore: deprecate get_current_extension_name (#2280) 2024-02-16 13:48:50 +02:00
526467747e Add Keycloak SSO (#2272)
* feat: add `keycloak` SSO

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-02-14 09:23:37 +01:00
Vlad StanandGitHub b8d295a5b7 refactor: generalize SSO auth (#2263)
* refactor: first extraction of providers
* refactor: remove unused property
* refactor: extract `_find_auth_provider_class`
* fix: return type
* feat: prepare for `keycloak`
2024-02-14 07:57:50 +01:00
jackstar12andGitHub 293b6267be fix: correct invoice expiry for lndrest (#2271) 2024-02-13 15:14:09 +02:00
Vlad StanandGitHub 20e70854a1 fix: add installed extensions to the ext list if missing (#2261) 2024-02-12 15:33:28 +01:00
dni ⚡andGitHub 11173e6460 fix: github release missing checkout (#2264)
actions/checkout was missing so it could not release
2024-02-12 13:00:03 +01:00
dni ⚡andGitHub 069a7922fc chore: update to 0.12.1 (#2262) 2024-02-12 12:43:20 +01:00
98338ffb00 Improves payment reactions (#2251)
* updated to select

* updating

* working

* bundle

* updated to select

* updating

* working

* bundle

* Make room for everything apart from sending

* Update lnbits/static/js/base.js

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

* prettier

* bundled

* fundle

* changed to window[]

* vlads suggestion

* added stars

* vlads suggestion

* fundle

---------

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

* fiddled formatting

---------

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

* Update .env.example

---------

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

* Working with toggle

* Prettier and bundle

* removed console.log

* bundle

* fix: i18n

* refactor: simplify logic

---------

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

added `workflow_dispatch` to be able to manually run those docker releases via github
2024-02-08 11:30:38 +01:00
ArcandGitHub f07b40735e Fixes annoying qr popup on parsing LNURL withdraw in wallet (#2230) 2024-02-07 12:08:49 +00:00
Pavol Rusnak 94f74d3f56 chore: make bundle 2024-02-06 14:45:13 -03:00
tlindiandPavol Rusnak d9a5e63e51 fixes to Finnish translation 2024-02-06 14:45:13 -03:00
dni ⚡andGitHub c8e5c23ac2 chore: update to latest fastapi (#2240)
security alert https://github.com/lnbits/lnbits/security/dependabot/46
2024-02-06 11:47:36 -03:00
Vlad StanandGitHub 59968e38d1 fix: check for username before allowing password change (#2239) 2024-02-06 11:48:13 +02:00
Pavol RusnakandGitHub 4f45781319 alby: fix balance reporting and error handling + opennode: fix error handling (#2221)
* opennode: fix error handling

* alby: fix balance reporting and error handling
2024-02-06 10:26:50 +02:00
Vlad StanandGitHub 22ea83e843 Fix extension navigation (#2236)
* fix: avoid `path` clash with other routes (`/withdraw` in this case)

* fix: refresh user state after extension action

* chore: fix `mypy`
2024-02-05 11:39:35 +00:00
dni ⚡andGitHub 55fed49be1 feat: use github release draft (#2237)
create a draft release on tagging
2024-02-05 12:01:11 +01:00
Pavol Rusnak 311d9a668b chore: make bundle 2024-02-02 17:25:13 -03:00
tlindiandPavol Rusnak 165f5093f4 add Finnish translation 2024-02-02 17:25:13 -03:00
43d92ddb51 [FEAT] release / docker ci pipeline (#1907)
* [FEAT] docker ci pipeline

* add release workflow

* depend on release

* move tag

* remove push to main

* fixup release workflow

* add latest tag on dockerhub

* cleanup

* Update .github/workflows/docker.yml

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

* use latest versions of actions

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-01-30 13:28:34 +00:00
dni ⚡andGitHub 10944bf100 refactor: add prepareFilterQuery to utils (#2219)
* refactor: add `prepareFilterQuery` to utils
factored out the preparation of the url params for a paginated table and its requests.
usermanager will also use that.
2024-01-30 08:05:39 +01:00
dni ⚡andGitHub ce9b2c3b77 refactor: move admin ui tabs around (#2218)
move authentication into security in preparation of user manager pr where users tab will be removed.
move service fee into server tab, i think its more fitting there.
2024-01-30 07:49:19 +01:00
dni ⚡andGitHub 4c102a08e8 refactor: topup wallet + into component (#2217)
* refactor: topup wallet `+` into component
this will also be used in the usermanager
* bundle
2024-01-30 07:48:59 +01:00
1d2b939e06 feat: cleanup-wallets cli command (#2176)
* feat: `cleanup-wallet` cli command
`lnbits-cli db cleanup-wallets` removes all wallets that never had an transaction.
this helps against spammers creating random wallets, eventually slowing down the db.
* add commands
* add to example env
* db_versions was used in app
* add delta as cli argument
* use days unit
* simplify cli argument name (cleanup_wallet_days -> days)!

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-01-30 07:47:15 +01:00
e1bb2113ed feat: install wizard on first launch (#1977)
* Login form loading

* add first install middleware and settings

* updates

* Login form loading

* add first install middleware and settings

* updates

* only set first install when superuser is created

* refactor first install

* only show if first install

* cleanup

* set password

* update calls

* login superuser on first install

* fix

* fixup!

* fixup!

* fixup!

* fixup!

* fixup!

* last fixup!

* fix mypy and prettier CI errors

* disable first install

* add random super user

* set first install after startup

* remove user id from form

* Update lnbits/core/views/auth_api.py

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

* Update lnbits/core/views/auth_api.py

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

* Update lnbits/middleware.py

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

* addressing Vlad's comments

* remove super user

* move to transient settings

* fix: show `first_install` page even after a server restart

* fix: do not add `user_id` in the auth token

* fix: `make check` errors

* fix: `username` is not optional for `UpdateSuperuserPassword`

* feat: nicer error message

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2024-01-25 13:33:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b71113700a chore(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#2203)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 08:17:12 +01:00
mrbppandGitHub ebfcc46c9e feat: improve PWA configuration (#2214)
---
PWA Errors:
---
- fix logo size
- add shortcut icon
- add maskable icons
- add desktop and phone screenshots (wide and narrow)
2024-01-23 08:14:37 +01:00
Pavol Rusnak c047ae6808 chore: make bundle 2024-01-22 10:23:14 -06:00
dni ⚡andPavol Rusnak 023a1a088e feat: add currency amount to lnurl/lnaddress payments
closes #2135
2024-01-22 10:23:14 -06:00
Pavol Rusnak 8eabf53642 introduce Wallet.normalize_endpoint to remove code duplication 2024-01-22 10:06:22 -06:00
Pavol Rusnak 824a8fa7c8 refactor __init__ functions of Wallets (funding sources)
- better error messages when a conf variable is not provided
- unify approaches among different source files
2024-01-22 10:06:22 -06:00
Pavol RusnakandGitHub a3b7c76523 sort CSV export of payments (#2208) 2024-01-22 08:03:31 -06:00
Vlad StanandGitHub 26ca8c71d7 Deactivate all extensions flag (#2206)
* feat: allow all extension deactivation

* doc: updated comment

* fix: make sure `register_routes` executes after installed extensions are checked

* chore: code format

* fix: do not run migration on deactivated extensions

* fix: make sure the deactivated extension list is loaded in time

* feat: register extension routes if extension never loaded before

* fix: move `load_disabled_extension_list`

* doc: disable by default
2024-01-22 12:18:12 +02:00
Vlad StanandGitHub 0d2447faf3 refactor: extract encrypt_internal_message and decrypt_internal_message (#2210) 2024-01-16 15:33:11 +00:00
Vlad StanandGitHub 4cea06c5a5 Move UI configs to profile (#2201)
* feat: add tabs

* feat: move buttons to user profile

* feat: i18n

* refactor: move ui methods to `account` component
2024-01-15 11:51:34 +02:00
Vlad StanandGitHub 031ce14857 refactor: extract AESCipher to crypto.py (#2202) 2024-01-15 11:51:15 +02:00
bd143f5c14 added directions for cache (#2197)
---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2024-01-10 20:09:08 -05:00
dni ⚡andGitHub 4e9c992a91 revert: migration (#2198)
took to long on legend to run, now taking current date as default
2024-01-09 11:34:53 +00:00
fusion44andPavol Rusnak d6ac3d2672 fix: remove custom devshells from the flake
The definition of the dev shell in the flake file had several
issues when opening a development shell. However, this is
unnecessary as mkPoetryApplication already creates a
default working dev shell.
2023-12-28 13:25:34 +01:00
dni ⚡andPavol Rusnak 1933d896cb chore: update to version 0.12.0 2023-12-23 12:15:03 +01:00
Pavol Rusnak 168a3bcddb chore: make bundle 2023-12-22 20:10:30 +01:00
Pavol Rusnak adc342ab9a reword API docs to API keys and API docs in the wallet accordeon 2023-12-22 20:10:30 +01:00
Pavol Rusnak bbf3cd96f1 move API docs as last item of wallet actions 2023-12-22 20:10:30 +01:00
e8c9304519 Simplified mobile wallet (#2175)
---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-22 17:32:01 +01:00
815c3e61e4 feat: add created_at and updated_at to wallets and accounts (#2177)
* feat: add `created_at` and `updated_at` to wallets and accounts

the title says it all :)

* fixup!

* nitpicks :)

* fixup!

* sqlite fix

* sqlite compat

* fixup!

* mypy

* revert db py

* motorinas suggestions

* int(time()) proper default values in migration

* uncomment migration

* use now = int(time()) idiom to make code more readable

also this fixes the issue where time() is called multiple times
providing different return values for multiple invocations

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-21 12:37:56 +00:00
Vlad StanandGitHub 4e55ea18e5 feat: remember last active wallet (#2189)
* feat: store last active wallet as a cookie

* fix: param check
2023-12-21 14:19:58 +02:00
Vlad StanandGitHub 81b70acf5f fix: position of method invocation (#2188)
* fix: move call to `checkUsrInUrl` outside the `if` branch

* chore: bundle update
2023-12-20 13:16:35 +00:00
callebtcandGitHub 165d7499ea LndResWallet: catch http errors (#2133) 2023-12-19 15:33:18 +00:00
Kalle RosenbaumandGitHub abbcfbeb6a cln: allow unpaid amountless invoices (#2138) 2023-12-19 12:12:52 +01:00
Pavol RusnakandGitHub bff44b7097 update nix ci action to use cachix to cache results (#2170) 2023-12-19 11:54:14 +01:00
Pavol RusnakandGitHub 95f6ae2bb6 chore: new translations (#2183)
* new translations
* chore: make bundle
2023-12-19 11:22:37 +01:00
Pavol RusnakandGitHub a15af87f9b fix make checkbundle which should not bump the serviceworker version (#2184) 2023-12-19 11:22:05 +01:00
97f8a4ab38 fix: urls for includes on print.html (#2185)
* fix the url for js libs
* fix the url for css

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-18 09:30:38 +01:00
HaosGamesandGitHub 7b9d370452 fix nixos module getExe warning (#2182) 2023-12-15 07:43:43 +01:00
Vlad StanandGitHub db72953310 Login polish (#2180)
* feat: set `samesite="none", secure=True` for `is_lnbits_user_authorized` (required in iframe)
* feat: add icons for User ID actions
* feat: set `samesite="none", secure=True
2023-12-14 15:07:22 +01:00
Vlad StanandGitHub bb918a8523 Login improve UI config (#2171)
* feat: show auth configs on the admin UI
* fix: do not access settings on load
* fix: redirect for click on item (not only on text)
* fix: remove `Display Name`
* fix: do not show `Verify email with` if no auth option is available
* feat: show warning before logout
* feat: i18n of account page
* fix: show account icon for user ID login
* fix: always check `isUserAuthorized`
* fix: update the `disclaimer_dialog` message
* feat: hide user ID by default
* fix: redirect from login page when user authorized
* feat: update logout message
* fix: do not translate company names
2023-12-14 11:34:23 +01:00
Tiago VasconcelosandGitHub 24b02cc656 fix not showing account for existing users (#2173) 2023-12-12 17:25:35 +00:00
Pavol RusnakandGitHub 9877ca602f use _blank for external links on main (#2172) 2023-12-12 15:08:30 +01:00
c9093715b7 [FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092)
no more superuser url!
delete cookie on logout
add usr login feature
fix node management
* Cleaned up login form
* CreateUser
* information leak
* cleaner parsing usr from url
* rename decorators
* login secret
* fix: add back `superuser` command
* chore: remove `fastapi_login`
* fix: extract `token` from cookie
* chore: prepare to extract user
* feat: check user
* chore: code clean-up
* feat: happy flow working
* fix: usr only login
* fix: user already logged in
* feat: check user in URL
* fix: verify password at DB level
* fix: do not show `Login` controls if user already logged in
* fix: separate login endpoints
* fix: remove `usr` param
* chore: update error message
* refactor: register method
* feat: logout
* chore: move comments
* fix: remove user auth check from API
* fix: user check unnecessary
* fix: redirect after logout
* chore: remove garbage files
* refactor: simplify constructor call
* fix: hide user icon if not authorized
* refactor: rename auth env vars
* chore: code clean-up
* fix: add types for `python-jose`
* fix: add types for `passlib`
* fix: return type
* feat: set default value for `auth_secret_key` to hash of super user
* fix: default value
* feat: rework login page
* feat: ui polishing
* feat: google auth
* feat: add google auth
* chore: remove `authlib` dependency
* refactor: extract `_handle_sso_login` method
* refactor: convert methods to `properties`
* refactor: rename: `user_api` to `auth_api`
* feat: store user info from SSO
* chore: re-arange the buttons
* feat: conditional rendering of login options
* feat: correctly render buttons
* fix: re-add `Claim Bitcoin` from the main page
* fix: create wallet must send new user
* fix:  no `username-password` auth method
* refactor: rename auth method
* fix: do not force API level UUID4 validation
* feat: add validation for username
* feat: add account page
* feat: update account
* feat: add `has_password` for user
* fix: email not editable
* feat: validate email for existing account
* fix: register check
* feat: reset password
* chore: code clean-up
* feat: handle token expired
* fix: only redirect if `text/html`
* refactor: remove `OAuth2PasswordRequestForm`
* chore: remove `python-multipart` dependency
* fix: handle no headers for exception
* feat: add back button on error screen
* feat: show user profile image
* fix: check account creation permissions
* fix: auth for internal api call
* chore: add some docs
* chore: code clean-up
* fix: rebase stuff
* fix: default value types
* refactor: customize error messages
* fix: move types libs to dev dependencies
* doc: specify the `Authorization callback URL`
* fix: pass missing superuser id in node ui test
* fix: keep usr param on wallet redirect
removing usr param causes an issue if the browser doesnt yet have an access token.
* fix: do not redirect if `wal` query param not present
* fix: add nativeBuildInputs and buildInputs overrides to flake.nix
* bump fastapi-sso to 0.9.0 which fixes some security issues
* refactor: move the `lnbits_admin_extensions` to decorators
* chore: bring package config from `dev`
* chore: re-add dependencies
* chore: re-add cev dependencies
* chore: re-add mypy ignores
* feat: i18n
* refactor: move admin ext check to decorator (fix after rebase)
* fix: label mapping
* fix: re-fetch user after first wallet was created
* fix: unlikely case that `user` is not found
* refactor translations (move '*' to code)
* reorganize deps in pyproject.toml, add comment
* update flake.lock and simplify flake.nix after upstreaming
overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose
were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463
* fix: more relaxed email verification (by @prusnak)
* fix: remove `\b` (boundaries) since we re using `fullmatch`
* chore: `make bundle`

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Arc <ben@arc.wales>
Co-authored-by: jackstar12 <jkranawetter05@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
Pavol RusnakandGitHub e76ba62b46 add vars related to service fee to .env.example (#2143) 2023-12-12 11:17:36 +01:00
Pavol RusnakandGitHub 2d00f4c41c add checkbundle to make check (#2169) 2023-12-12 11:13:58 +01:00
Pavol RusnakandGitHub 3ad18ca365 chore: update to version 0.11.3 (#2168) 2023-12-12 11:13:43 +01:00
Pavol RusnakandGitHub cf4d575062 add i18n checker and autogenerated AI translations (#2160)
* use translated string

* fix typos in lnbits/static/i18n/{it,jp,nl,we}.js

* add missing strings to cs,en,sk translations

* remove duplicates from lnbits/static/i18n/{cs,en,kr,sk}.js

* add i18n checker

* add i18n ai tool

* add autogenerated AI translations

* add i18n-ai-tool check whether variables in formatted strings are not broken

* fix issues with variables found by the script

* chore: make bundle
2023-12-12 10:10:51 +00:00
saunterandGitHub 9f835f8350 swap Alby logos (#2148) 2023-12-11 20:40:57 +01:00
297c42371d feat: handle intents (#2161)
if lightning/lnurl param is present in /wallet, it will pass to the decode dialogue

* chore: code format
* fix: call to `decodeRequest`
* feat: set `position="top"` for all dialogs (avoid mobile keyboard overlap)

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2023-12-11 20:40:22 +01:00
dni ⚡andGitHub 8352223704 fix: lnurl should follow redirect (#2167)
`r.raise_for_status` raises an proper exception now
2023-12-11 11:57:36 +01:00
Pavol Rusnak f9c9c52d75 use _blank + noopener + noreferrer consistently 2023-12-11 10:51:21 +01:00
Haos GamesandPavol Rusnak 7704029e59 add option to pass additional variables 2023-12-11 08:33:16 +01:00
Tiago VasconcelosandGitHub a83fb13335 add fiat balance to wallet (#2157)
also fixed payments table fiat denomination for consistency
2023-12-06 17:54:58 +01:00
Pavol Rusnak b8e6e80bdf fix: nix build 2023-12-06 12:55:01 +01:00
dni ⚡andPavol Rusnak ba90bc5fea feat: lnurl library update
follow up on https://github.com/lnbits/lnbits/pull/1864/files
2023-12-06 12:55:01 +01:00
dni ⚡andPavol Rusnak d7257af7a0 bundle 2023-12-06 11:56:26 +01:00
dni ⚡andPavol Rusnak f36771db9d vlad nitpick 2023-12-06 11:56:26 +01:00
dni ⚡andPavol Rusnak aa68d67b5e feat: add password to <lnbits-dynamic-fields> 2023-12-06 11:56:26 +01:00
dni ⚡andPavol Rusnak 5fc472a8d9 refactor: move webpush to it own router
refactor to make `views/api.py` cleaner
2023-12-06 11:13:24 +01:00
dni ⚡andGitHub fa49133bd7 refactor: move tinyurl to its own router (#2154)
small little refactor getting to a cleaner `views/api.py`
2023-12-06 10:54:40 +01:00
47bd96246f chore: update lock file and poetry 1.7.0 in CI (#2156)
* chore: update lock file and poetry 1.7.0 in CI
* mypy found an issue! :)

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-06 10:36:10 +01:00
dni ⚡andGitHub cae39bc19f fix: branch out delete payment (#2152) 2023-12-05 12:40:45 +01:00
callebtcandGitHub 4839cdf2f8 fix: fee reserve for internal (#2150) 2023-12-05 10:31:53 +00:00
Tiago VasconcelosandPavol Rusnak 08571368c0 restore ad space title 2023-12-02 23:15:42 +01:00
dni ⚡andGitHub 9fcf566540 fix: time margin for deleting failed payments (#2147)
this pr adds a 15 minutes margin before deleting failed outgoing payments.
2023-12-02 00:41:09 +01:00
Pavol Rusnak 5f36800100 use awk to bump CACHE_VERSION (drop sed usage) 2023-12-01 10:27:41 +01:00
e46b12d267 fix issue with displaying languages (#2130)
* fix issue with displaying languages

* readd tooltip

* remove all raw stuff

* fix api docs tooltip

* fix remaining {% raw %} + $t() usage

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-11-30 12:55:46 +00:00
Pavol RusnakandGitHub 62b0e3fe89 set User-Agent when accessing external resources (#2100)
* set User-Agent when accessing external resources

* refactor User-Agent into settings.user_agent
2023-11-30 12:54:07 +00:00
Pavol RusnakandGitHub 992e3bfb9a optimize PNG images in lnbits/static/images for size (#2131)
* optimize PNG images in lnbits/static/images for size

* remove lnbits/static/extension.png which is not used anywhere

* add tools/optipng.sh
2023-11-30 12:50:19 +00:00
BitkarrotandGitHub 43b8c744e6 add Alby logo to front index page (#2128) 2023-11-28 12:01:46 +01:00
d0107f7830 ci: workflow to check bundle files (#2126)
fail if `bundle.min.css` or `bundle.min.js` differes from a `make bundle` output

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-11-28 10:39:21 +01:00
b66396857b feat: add libwally for liquid support (#2117)
* feat: add libwally for liquid support
this is needed for liquid support in the new boltz extension, sadly i could not get it to work with embit library, liquid support is not quite ready there and now documentation or tests available
* fix: prefer wheels for wallycore on nix
* feat: make wallycore dep optional, introduce "liquid" poetry extra
for Liquid support install deps via `poetry install -E liquid`

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-11-27 10:45:16 +01:00
9c85e1156c feat: manage extensions navbar (#2121)
* feat: manage extensions navbar
create now `Manage` spacer in sidebar. and add manage extensions to it
closes #2088
* do not show when no exts
* update cn and jp i18n strings
* rename lnbits-admin-ui component to lnbits-manage, fix its behaviour
* i18n: add Korean language

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: psychoet <125599543+psychoet@users.noreply.github.com>
2023-11-27 10:40:36 +01:00
Vlad StanandGitHub 3b0024bcf2 fix: redirect for all paths of the extension (not only for /api) (#2120)
* fix: redirect for all paths of the extension (not only for `/api`)
* refactor: rename variable
* fix: corner case for root path
* refactor: rename var
* doc: update comment
* fix: do not redirect for static resources
2023-11-24 21:13:19 +01:00
Sebastian HagensandGitHub 4c4f0922a4 change path to super_user file located in the data directory (#2115) 2023-11-22 17:30:30 +01:00
Pavol RusnakandGitHub 8e63756c61 docs: small cleanup (#2107) 2023-11-21 22:30:52 +01:00
callebtcandGitHub 0eb74c86ba CLN: return payment failure if no payment is found (#2113)
* CLN: return payment failure if no payment is found
2023-11-21 12:54:55 +01:00
ArcandGitHub e65ec56276 Organised env (#2110)
* Shuffled env around

Env has become bigger, and is a bit disorientating for users

* removed whitespace

* shuffle

* optimised for smaller screen

* Made ADMIN UI stuff more prominent
2023-11-21 11:26:53 +00:00
Tiago VasconcelosandGitHub 90c54f2c30 pass custom extra on payment out (#2089) 2023-11-21 12:20:08 +01:00
6a27b91fcb Add service fee to specific wallet (#2050)
* add service fee to specific wallet

* add to .env.example

* Added service fee wallet to manage server

* cleaned

* prettier

* Added badge for service fee

* Added tooltip

* Added service fee max

* allow ignoring service fee for internal transactions

* add fee_reserve_total helper funciton that includes service_fee

* html for admin ui

* typo

* Update .env.example

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

* fix .env.template comment

* bundle

* WIP: expose fee reserve endpoint

---------

Co-authored-by: Arc <ben@arc.wales>
Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-11-21 11:11:21 +00:00
callebtcandGitHub 4dcf26bcb3 Update to c-lightning-REST v0.10.5 (#2109)
* Update to c-lightning-REST v0.10.5
* update invoice stream
2023-11-18 12:14:29 +01:00
d5ae1e3d6a feat: add getalby wallet as funding source (#2086)
* initial scaffolding, methods for getalby wallet
* Add getAlby to docs and i18n
* update alby wallet methods
* change names from GetAlbyWallet to AlbyWallet
* remove unused variables in AlbyFundingSource in settings.py
* rename getalby.py to alby.py
* fix method auth and status
* resolve rolznz commented issues
* rename ALBY_API_KEY to ALBY_ACCESS_TOKEN
* fix desc hash in create_invoice

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: dni  <office@dnilabs.com>
2023-11-14 20:28:25 +01:00
dni ⚡andGitHub 6f0d911a08 fix: dont release docker image for release candidate (#2101)
- do not release docker image on `0.11.2-rc1`
- only release on `0.11.x` or `v0.11.x`
- also remove invalid version `0.0.0.0`
2023-11-14 18:59:21 +01:00
callebtcandPavol Rusnak 3e91200d3f FakeWallet: provide payment preimage 2023-11-13 15:42:57 +01:00
fiatjafandPavol Rusnak f989eba2c9 Api -> API 2023-11-13 15:42:57 +01:00
11d57e9d52 hotfix toolbar title (#2096)
fix button wrapping on toolbar

Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2023-11-13 14:42:01 +00:00
Pavol Rusnak 62fa22098e change color theme icons 2023-11-13 15:41:11 +01:00
ArcandGitHub 4417d53562 Updated nix install guide (#2099)
* Updated nix install guide

* typo
2023-11-13 14:28:52 +00:00
Vlad StanandGitHub b380108a4f doc: add SUPER_USER field (#2097) 2023-11-13 14:10:53 +02:00
dni ⚡andPavol Rusnak 87a42087de bundle 2023-11-13 11:21:29 +01:00
dependabot[bot]andPavol Rusnak bee857c2af Bump axios from 1.3.4 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 1.3.4 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.4...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 11:21:29 +01:00
Sébastien KrafftandPavol Rusnak 31782040c6 formatted code 2023-11-12 22:53:03 +01:00
Sébastien KrafftandPavol Rusnak 1195dd3eb3 Fix according to suggestion from prusnak
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-11-12 22:53:03 +01:00
Sébastien KrafftandPavol Rusnak a61d74deff Fixed payment isn't initiated on lndrest 2023-11-12 22:53:03 +01:00
Pavol RusnakandGitHub d1b89bc68a dotenv is not required directly, only indirectly (#2087)
via environs and pydantic (both already present in pyproject.toml)
2023-11-08 08:54:46 +01:00
dni ⚡andGitHub 5f8faf237c chore: remove boltz settings (#2019)
from `settings.py`
2023-11-08 08:53:53 +01:00
Pavol Rusnak 5b62f1c586 make example paths in .env.example less confusing 2023-11-07 18:25:01 +01:00
Pavol Rusnak 7306df3445 handle correctly the case with no active channels 2023-11-07 13:03:16 +01:00
Pavol Rusnak 10fb35b47f make bundle 2023-11-06 15:17:47 +01:00
Pavol Rusnak 0ffba99eaf i18n: add flags to languages 2023-11-06 15:17:47 +01:00
Pavol Rusnak 18ebe010bc i18n: add Czech (cs) and Slovak (sk) localization 2023-11-06 15:17:47 +01:00
Pavol RusnakandGitHub 4817a62a66 i18n: remove empty lines (#2079) 2023-11-06 10:54:44 +01:00
Pavol RusnakandGitHub 5473d9e940 add comment why CLightningWallet alias should be kept (#2065) 2023-11-06 10:20:26 +01:00
Pavol RusnakandGitHub 1ab68e80b3 use floor() instead of round() to convert msat to sat in frontend (#2070)
* use floor() instead of round() to convert msat to sat in frontend
2023-11-06 07:49:47 +01:00
dni ⚡andPavol Rusnak 3e142fab0f feat: logger success when wallet is connected
this should be a success log, see screenshot
2023-11-01 20:32:59 +01:00
460ceeba1d feat: add extension-settings vue component (#2004)
- adds lnbits-extension-settings-form. which is just the plain form of the settings component.
- adds lnbits-extension-settings-btn-dialog. which is the form wrapped into a dialog with a cog button.

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2023-10-30 08:09:53 +01:00
jackstar12andGitHub 14e678f661 Merge pull request #2068 from TrezorHannes/dev
Enable Node Management via .env without activating admin-UI
2023-10-28 22:07:51 +02:00
a2469abebd Enable Node Management via .env
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>
2023-10-28 18:53:55 +02:00
Pavol Rusnak 3b01af242c remove unnecessary str() in f-string 2023-10-27 14:05:16 +02:00
4d1c4f6348 [FEAT] add cache busting via static_url_for and settings.cache_version (#1964)
closes #1954
this PR add cache busting to `/static`
additionally i combined `lnbits/core/static` with `lnbits/static`, it was not necessary and added a lot of duplicate code for cache busting. now you have to include all static files inside the html files with `{{ static_url_for("static", "app.css" ) }}`

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-10-27 13:50:49 +02:00
Kalle RosenbaumandGitHub fed2d41139 CLN: Handle optional invoice fields (#2056) 2023-10-26 00:47:35 +02:00
c8147bd83f [FEAT] add extension functionality to lnbits-cli (#1963)
* [FEAT] add extension functionality to lnbits-cli

WIP

draft cli commands for vlad :)

* add extension list command

* [feat] lnbits-cli add install, uninstall and upgrade

* feat: load settings from DB

* refactor: simplify settings loading

* feat: show current version if installed

* feat: add mor emessages

* feat: basic DB install

* feat: add extension

* feat: do not install if the server is up

* feat: add logic for uninstall

* refactor: prepare for upgrade

* feat: check extension before upgrade

* refactor: stuff

* fix: have a default value

* feat: use the API logic

* feat: use pi methods for un-install

* refactor: extract _select_release

* feat: add flags

* feat: check if extension already up to date

* refactor: use `_run_async`

* feat: install all extensions

* feat: install online

* fix: api install

* fix: API upgrade & install

* feat: add API uninstall

* failed typo

* typo running

* url duplication

* [fix] provide short-options too (same as upgrade command)

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

* make black

* fix: fail if .superuser file not found; add `--admin-user` option

* fix: ambiguous use of `logger.debug`

- register_new_ext_routes must not be None
- `logger.debug` was used because it allowed any arguments, but that was a bad idea
- now an explicit empty `_do_nothing(*_)` function is used

* fix: load settings

* doc: updated `--source-repo`

* chore: rename `upgrade` to `update`

* refactor: use `@annotation` for making commands async

* fix: code checks

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-10-25 14:03:00 +02:00
callebtcandGitHub 29e779d09d Merge pull request #2061 from lnbits/main
Sync main->dev
2023-10-25 10:03:58 +02:00
Pavol RusnakandGitHub 3a134672e9 Update docs/guide/installation.md (#2059) 2023-10-25 08:28:42 +02:00
callebtcandGitHub 6b52c42b2b Revert "Update installation.md (#2052)" (#2060)
This reverts commit ddd27fe426.
2023-10-24 12:39:07 +02:00
arbadacarbaandPavol Rusnak 811292529b Update api.md
Swappger link wrong
2023-10-23 17:18:09 +02:00
Vlad StanandGitHub 5262f56aaf fix: emit event for nested lnbits-dynamic-fields components (#2053)
* fix: emit event for nested components

* chore: update bundle
2023-10-20 14:14:10 +03:00
MaxBipalandGitHub ddd27fe426 Update installation.md (#2052)
fixed cli error
2023-10-20 13:13:42 +02:00
dni ⚡andPavol Rusnak c15beaa014 chore: bump version to 0.11.2
in preparation of the next release
2023-10-18 17:21:31 +02:00
Vlad StanandGitHub 1c38b129c0 [BUG] Installation of new extensions via LNBITS_EXTENSIONS_DEFAULT_INSTALL does not select the right version (#2046)
* fix: guard against bad versions (eg: `v0.2-ui-api-updates`)

* fix: use the latest release on auto-install
2023-10-18 14:11:58 +03:00
ArcandPavol Rusnak a411de1d11 Update installation.md (#1997) 2023-10-17 11:24:06 +02:00
e64810d96a fix: closed channels affecting the stats (#1998)
Co-authored-by: jackstar12 <jkranawetter05@gmail.com>
2023-10-17 11:23:57 +02:00
Vlad StanandGitHub d64e2f42f5 fix: explicitly specify the DB name when migrating (#2039) 2023-10-17 10:56:38 +02:00
dni ⚡andPavol Rusnak e522a53340 formatting 2023-10-16 15:49:56 +02:00
Pavol Rusnak c02e0618af add "nix build" to GitHub CI
run only when poetry/flake files change
2023-10-16 15:49:56 +02:00
Pavol Rusnak 1226d2e6d8 update poetry.lock 2023-10-16 15:49:25 +02:00
bitkarrotandPavol Rusnak 613c86f5e2 add python-crontab for scheduler extension 2023-10-16 15:49:25 +02:00
Pavol Rusnak 018d2fe800 run optipng -o 7 on PNG logos again 2023-10-16 14:53:56 +02:00
dni ⚡andPavol Rusnak 033c5f30aa optimize SVG
update PNG
2023-10-16 14:53:56 +02:00
Pavol Rusnak 647f53821b run optipng -o 7 on PNG logos to reduce their size 2023-10-16 14:53:56 +02:00
Pavol Rusnak 3bac759cab remove old logo from docs 2023-10-16 14:53:56 +02:00
dni ⚡andGitHub dbf687c47a feat: add useful query helpers for insert and update (#2020)
* feat: add useful query helpers for insert and update

i saw this bits of code all over the codebase, this `helpers.py`
tries to abstract that away and make it more readable.
2023-10-16 13:27:59 +02:00
dni ⚡andPavol Rusnak c8661ffcf3 feat: improve im workflows runs
only run push on main / dev
2023-10-16 11:39:40 +02:00
Pavol RusnakandGitHub 00c3b69c7a downgrade rpds-py to 0.10.3 which is not broken on nix (#2027) 2023-10-12 20:14:16 +02:00
Pavol RusnakandGitHub 6457f7b5e6 use PNG logo for QR code instead of SVG logo (#2028)
SVG causes rendering issues on some browsers unfortunately
2023-10-12 12:59:02 +02:00
4ec6a055d9 feat: add file logging (#2023)
logs into `lnbits.log` with info loglevel and `debug.log` with DEBUG loglevel
files will be saved into `LNBITS_DATA_DIR`.
retentions is configurable but defaults to `90 days`
* added log rotation aswell

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2023-10-12 11:24:05 +02:00
Vlad StanandPavol Rusnak 484e1ae78a chore: make bundle 2023-10-11 16:16:41 +02:00
Vlad StanandPavol Rusnak ba6408b944 feat: add lnbits-dynamic-fields 2023-10-11 16:16:41 +02:00
Pavol Rusnak dc7ce1e43c make bundle 2023-10-11 15:45:42 +02:00
Tiago VasconcelosandPavol Rusnak 371dacf8b2 make QR img bigger, adjust css 2023-10-11 15:45:42 +02:00
Tiago VasconcelosandPavol Rusnak ebaad7fe49 q-btn inside block 2023-10-11 15:45:42 +02:00
Pavol RusnakandGitHub ba0e431199 add possibility to turn off new registrations (#2017)
with LNBITS_ALLOW_NEW_ACCOUNTS=false

while keeping existing users functional (i.e. no allowlist for existing users)
2023-10-11 13:46:27 +02:00
b2384c10cc Refactor: Calculate invoice expiry outside of the crud (#1849)
* expiry before crud
* using Bolt11 as argument instead of payment_request
* fix missing expiry

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-10-10 12:03:24 +02:00
80529dee4b feat: improve on admin ui funding tab (#1989)
* feat: improve on admin ui funding tab
* now only shows settings of selected fundingsource.
* refactor into vue component
* use camel case
* refactor: move admin js into .js file (#1990)
* updateFundingData is redundent now

---------

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2023-10-10 12:02:40 +02:00
c2a11fa0bb Add a customizable logo to QR (#1955)
* add QR_LOGO variable
* add logo to images folder
* css for overlaying logo on qr
* qr url on settings
* add qr url to global env
* add QR url input on theme tab in adminUI
* new component added
* use svg
* remove white border around logo in the QR

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
Co-authored-by: dni  <office@dnilabs.com>
2023-10-09 21:11:11 +02:00
dni ⚡andGitHub f286a91253 bug: check prettier workflows (#2021)
workflow wasnt failing on wrong formatting
2023-10-09 18:15:44 +02:00
jackstar12andGitHub a937e09f85 fix: make bolt11 optional in NodePayment (#2005) 2023-10-09 13:02:22 +02:00
jackstar12andGitHub 219838244b feat: fix filename in payments export (#1991) 2023-10-09 12:45:14 +02:00
dni ⚡andGitHub 54e0d1aa15 [CHORE] cleanup ip block middle workaround for older fastapi (#1981) 2023-10-09 07:57:13 +02:00
daywalker90andGitHub 829e1b8f05 remove comma after version (#2016) 2023-10-09 07:53:08 +02:00
dni ⚡andGitHub 4a14cb9f41 [CHORE] cleanup nodemanagement vue components (#1986)
lnbits-date component was a duplication.
moved all to the Vue.components into node.js
empty {% raw %} tags in public
2023-10-07 23:50:52 +02:00
Pavol RusnakandGitHub cb746056e0 dependency cleanup (#2011)
- async-timeout is not used anywhere (nor core neither extensions)
- cerberus is not used anywhere (nor core neither extensions)
- move embit and secp256k1 to end of the list
  - these are not used by core
  - document which extensions use them
2023-10-07 21:06:58 +02:00
Pavol RusnakandGitHub c1b3938d5d use current version of logo for manifest icon (#2010) 2023-10-07 10:38:37 +02:00
jackstar12andGitHub 6e5fbb92e8 fix: use correct field from response (old one got removed) (#1999) 2023-10-06 17:13:22 +02:00
dni ⚡andGitHub 9f847ffcad hotfix: add environs backwards compatibility (#2007)
for the time beeing its better to readd this package for backwards compatibility in LNURLp and cashu
2023-10-06 16:28:34 +02:00
dni ⚡andGitHub b51daedb50 update: to 0.11.1 (#1988) 2023-10-06 09:13:33 +02:00
Pavol RusnakandGitHub 4dfce8f0fa drop version from readme (#2001) 2023-10-06 08:59:02 +02:00
dni ⚡ bcdf1f401f revert: update 2023-10-04 14:00:07 +02:00
dni ⚡ a3816a90b6 update: to 0.11.1 2023-10-04 13:58:11 +02:00
dni ⚡andGitHub 33f4c6fde7 fix: admin_ui js error (#1982)
* fix: admin_ui js error

undefined formData.lnbits_allowed_currencies JS error in console

* vlad suggestion
2023-10-04 12:23:27 +02:00
callebtcandGitHub 36445732d2 catch exceptions in migrations for extensions (#1987) 2023-10-04 12:09:54 +02:00
c5192146af nix: fix nixosModule and build (#1979)
* lnbits/settings: get LNBITS_COMMIT from envvar if available

There is no other way of setting the git commit at runtime, for build systems like Nix that allow you to hermetically define envvars derived from the source that will be available reproducibly at runtime

* nix: update potree2nix and nixosModule

This is a general refactor that gets everything building and passing the vmTest again

* Update nix/modules/lnbits-service.nix

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-10-03 09:50:35 +01:00
Vlad StanandGitHub 542273a8d9 feat: remove commit version (#1980) 2023-10-02 20:31:06 +01:00
52608c4ffc Add Markdown capability globally (#1965)
* add markdown capability globally

* add markdown to site description

* add showdown to package.json

remove it from vendor.json, its bundled

* formatting

* io

* Update lnbits/core/templates/core/index.html

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-09-27 11:32:03 +01:00
jackstar12andGitHub 5c5dd98af2 fix: catch sse exceptions (#1971) 2023-09-27 11:25:42 +02:00
Vlad StanandGitHub 2971b571bc [BUG] Fix bad transation call (#1973)
* fix: `:label="$('xxx')` to :label="$t('xxx')

* chore: commit bundles

* chore: remove extra file
2023-09-27 11:57:02 +03:00
dni ⚡andGitHub 69703b0a05 [BUG] bundle is broken (#1972) 2023-09-27 10:47:55 +02:00
jackstar12andGitHub 97cc3c5adc fix: node ranks (#1968) 2023-09-27 08:02:53 +02:00
Pavol RusnakandGitHub b13c470d54 fix typos (#1969) 2023-09-27 08:02:28 +02:00
d79a55b4c6 Don't create user if allowed users is set (#1822)
* dont allow creating of users when allowed users are set
* add info to .env.example

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-09-26 08:08:36 +02:00
dni ⚡andGitHub 5b16f54857 [FEAT] cleanup GET /wallet endpoint, add wallet api routes (#1932)
* [FEAT] cleanup GET /wallet endpoint, add wallet api route
this removes the functionalitiy to create accounts and wallets via
the GET /wallet endpoint in generic.py

it add endpoints inside the api.py for it and the frontend is modified to use the api endpoints

this also simplifies for the `feat/login` for the route.

* remove stale generic tests and add api tests

* bug wrong endpoint create account

* vlad nitpick

* added checkif deleted is 404

* reload after renaming wallet

* another iteration with vlad

* create new wallet if it none exist

* fix delete refresh

* formatting
2023-09-25 14:06:00 +01:00
eb73daffe9 [FEAT] Node Managment (#1895)
* [FEAT] Node Managment

feat: node dashboard channels and transactions
fix: update channel variables
better types
refactor ui
add onchain balances and backend_name
mock values for fake wallet
remove app tab
start implementing peers and channel management
peer and channel management
implement channel closing
add channel states, better errors
seperate payments and invoices on transactions tab
display total channel balance
feat: optional public page
feat: show node address
fix: port conversion
feat: details dialog on transactions
fix: peer info without alias
fix: rename channel balances
small improvements to channels tab
feat: pagination on transactions tab
test caching transactions
refactor: move WALLET into wallets module
fix: backwards compatibility
refactor: move get_node_class to nodes modules
post merge bundle fundle
feat: disconnect peer
feat: initial lnd support
only use filtered channels for total balance
adjust closing logic
add basic node tests
add setting for disabling transactions tab
revert unnecessary changes
add tests for invoices and payments
improve payment and invoice implementations
the previously used invoice fixture has a session scope, but a new invoice is required
tests and bug fixes for channels api
use query instead of body in channel delete
delete requests should generally not use a body
take node id through path instead of body for delete endpoint
add peer management tests
more tests for errors
improve error handling
rename id and pubkey to peer_id for consistency
remove dead code
fix http status codes
make cache keys safer
cache node public info
comments for node settings
rename node prop in frontend
adjust tests to new status codes
cln: use amount_msat instead of value for onchain balance
turn transactions tab off by default
enable transactions in tests
only allow super user to create or delete
fix prop name in admin navbar

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-25 15:04:44 +02:00
c536df0dae Support custom path for installed extensions (#1940)
* feat: the more you fuck around the more you learn
* feat: backwards compatible static file loading
* refactor: update paths for extension files
* refactor: var renaming
* doc: update `LNBITS_EXTENSIONS_PATH` documentation
* fix: default folder install
* feat: install ext without external path
* doc: `PYTHONPATH` no longer required
* fix: add warnings
* fix: missing path
* refactor: re-order statements
* fix: hardcoded path separator

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-09-25 12:44:29 +02:00
50561a8696 Startup optimization: nonblocking expiry check (#1943)
* nonblocking expiry check
* autoruff
* smaller interval

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-09-25 12:32:01 +02:00
blackcoffeexbtandGitHub 7a182244b9 Updated guide for extension development for current extension management process (#1962) 2023-09-25 11:19:30 +01:00
dni ⚡andGitHub 1646b087cf adding bolt11 lib and removing bolt11.py from the codebase (#1817)
* add latest bolt11 lib

decode exception handling for create_payment
fix json response for decode
bugfix hexing description hash
improvement on bolt11 lib
update to bolt11 2.0.1
fix clnrest
* bolt 2.0.4
* refactor core/crud.py

* catch bolt11 erxception clnrest
2023-09-25 12:06:54 +02:00
Pavol RusnakandGitHub bd1db0c919 add editorconfig config, fix issues (only whitespace changes) (#1958) 2023-09-25 11:06:25 +01:00
193c751670 [FEAT] Focus cursor to textarea (#1959)
* Focus cursor to textarea 

Fix need for additional click to textarea before pasting

* [FEAT] autofocus textarea on paste request

closes #1959

* format arba

* bundle

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-09-25 10:59:04 +01:00
a8082f798f [CHORE] update dependencies, unsafe pip packages, fastapi (#1609)
* d

* middleware needs to be initialised before startup

runs on python3.11

update secp256k1

update psycopg

* update fastapi to v0.103

* fix webpush

* bump dependencies

* ruff issue

* lock versions

* bump versions

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-25 10:54:02 +01:00
callebtcandGitHub d3d1010360 remove LNURL-p description_hash validation (#1951) 2023-09-20 12:09:48 +02:00
ArcandGitHub 3260acbcc7 Revert "do not show deleted wallets (#1942)" (#1947)
This reverts commit 1ee2d53069.
2023-09-18 19:41:35 +01:00
callebtcandGitHub b8922a7cd7 extension loader: do not panic! (#1945) 2023-09-18 19:29:10 +01:00
Tiago VasconcelosandGitHub 1ee2d53069 do not show deleted wallets (#1942)
* do not show deleted wallets

* rename parameter for clarity
2023-09-18 19:21:20 +01:00
dni ⚡andGitHub 6acbf823ae [CHORE] bugfix formatting prettier (#1936) 2023-09-12 20:05:43 +01:00
jackstar12andGitHub 4c16675b3b Fix payments chart (#1851)
* feat: payment history endpoint

* test payment history

* use new endpoint in frontend

* refactor tests
2023-09-12 13:38:30 +01:00
f526a93b6c make wallets name prettier! (#1918)
* make wallets name prettier!

* Update lnbits/core/templates/admin/index.html

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

* Update lnbits/core/templates/admin/index.html

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

---------

Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2023-09-12 13:51:56 +02:00
fee40d7321 [REFACTOR] core/__init__ to not have circular import issues (#1876)
* F541 fix

remove unused workflow and combine linters into one

add lnbits/static to ruff ignore
remote setupnode

ignore upgrades for mypy

ignore F401 for __init__ files

unused noqa

ignore upgrades for black

F821: undefine name

disabled and logged webhook_listener for opennode and lnpay because they are obvisouly not working

E402: module level import not at top of file

fixup

revert breaking changes wait for PR #1876

https://github.com/lnbits/lnbits/pull/1876

E721 fixes, only popped up for python3.9 not 3.10

[REFACTOR] core/__init__ to not have circular import issues

WIP

add db for backwards compat

fix pyright

make mypy happy again

pyright did not catch those, i think mypy got confused with relative imports. maybe we should use absolute ones everywhere

E402: module level import not at top of file

dont forget to add core_app

rebase on ruff pr

f

remo

format

* fix clnrest

* ignore E402 in conftest

* refactoring issues

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-12 11:25:05 +01:00
f19d59d429 [BUG] self payments for the same wallet (#1914)
* [BUG] self payments for fakewallet

make it possible to pay to yourself on fakewallet

f

* bugfix selfpayments for fakewallet

* delete by wallet and fix previous_payment check (#1919)

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-09-12 11:23:23 +01:00
jackstar12andGitHub a97f298586 [TEST] use test data as mockdata (#1929)
* add db group to cli

* delete mock_data.zip

* generate migration data through tests
2023-09-12 11:21:05 +01:00
0b3d5bbe0e [CORE] Document CoreLightningRestWallet (#1920)
* document CoreLightningRestWallet
* pushed to comment
* fixed base64 comment

---------

Co-authored-by: Ben <ben@arc.wales>
2023-09-12 12:14:47 +02:00
jackstar12andGitHub c29e7a1759 [TEST] use clean db in postgres tests (#1928) 2023-09-12 11:02:01 +01:00
bda054415a [FEAT] improve update_admin_settings (#1903)
* [FEAT] improve update_admin_settings

while working on the push notification pr i found it very hard just to update
2 settings inside the db, so i improved upon update_admin_settings.
now you just need to provide a dict with key/values you want to update inside db.

also debugging the endpoints for update_settings i found despite the type of `EditableSettings`
fastapi did in fact pass a dict.

* t

* use `EditableSettings` as param in update_settings

* fix settings model validation

we previously overrode the pydantic validation with our own method

* make `LnbitsSettings` a `BaseModel` and only add `BaseSettings` later

this allows us to instantiate `EditableSettings` without the environment values being loaded in

* add test

* forbid extra fields in update api

* fixup

* add test

* test datadir

* move UpdateSettings

* fix compat

* fixup webpush

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-12 10:59:32 +01:00
jackstar12andGitHub 1b2db34e08 [FEAT] create data folder if it doesnt exist (#1930) 2023-09-12 11:56:21 +02:00
dni ⚡andGitHub 28e4612559 [FIX] codeql for dev branch (#1935) 2023-09-12 10:17:40 +01:00
dni ⚡andGitHub 301079d4e1 [CHORE] update to version 0.11.0 (#1933) 2023-09-12 10:16:04 +01:00
jackstar12andGitHub 6360f23495 [FIX] correct amount in fiat tracking (#1934)
* better debug log

* fix: convert msat to sat for fiat amount
2023-09-12 10:06:35 +01:00
fb98576431 [FEAT] Push notification integration into core (#1393)
* push notification integration into core

added missing component

fixed bell working on all pages
- made pubkey global template env var
- had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py`

formay

trying to fix mypy

added py-vapid to requirements

Trying to fix stub mypy issue

* removed key files

* webpush key pair is saved in db `webpush_settings`

* removed lnaddress extension changes

* support for multi user account subscriptions, subscriptions are stored user based

fixed syntax error

fixed syntax error

removed unused line

* fixed subscribed user storage with local storage, no get request required

* method is singular now

* cleanup unsubscribed or expired push subscriptions

fixed flake8 errors

fixed poetry errors

* updating to latest lnbits

formatting, rebase error

fix

* remove unused?

* revert

* relock

* remove

* do not create settings table use adminsettings

mypy

fix

* cleanup old code

* catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage

* show notification bell on user related pages only

* use local storage with one key like array, some refactoring

* fixed crud import

* fixed too long line

* removed unused imports

* ruff

* make webpush editable

* fixed privkey encoding

* fix ruff

* fix migration

---------

Co-authored-by: schneimi <admin@schneimi.de>
Co-authored-by: schneimi <dev@schneimi.de>
Co-authored-by: dni  <office@dnilabs.com>
2023-09-11 14:48:49 +01:00
dni ⚡andGitHub 8f0c1f6a80 [REFACTOR] grpcio and protofbuf for lndgrpc is not optional (#1886)
also removed dead code def get_ssl_context

black
2023-09-11 14:24:37 +01:00
576e20d0cd Add deleted flag wallet (#1826)
* add deleted flag on wallets

set deleted on delete wallet

2 twelves

format

fail on create invoice

make deleted check on SQL query

nazi flake8

add_test

boom... it works and passes!!

* add app fixture

vlad's recommendations

add deleted

* Add deleted flag to Wallet

* restore crud

* do not check for wallet in services.py

* add deleted flag on wallets

set deleted on delete wallet

2 twelves

format

fail on create invoice

make deleted check on SQL query

nazi flake8

add_test

boom... it works and passes!!

* add app fixture

vlad's recommendations

* add deleted

* error checks

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-09-11 14:06:31 +01:00
dni ⚡andGitHub 6773a0f533 [REFACTOR] .super_user move it into data dir (#1900)
related to https://github.com/lnbits/lnbits/pull/1855
put the `.super_user` into our data_dir to not pollute our root dir
removed the weird mention to the url in the logs and mention
the `lnbits-cli superuser` instead

this is a breaking change for the bundlers, `cat .super_user` is not good anymore.
fix would be `cat data/.super_user` but for future compatibility i recommend
using `poetry run lnbits-cli superuser`
2023-09-11 12:19:19 +01:00
Djuri BaarsandGitHub 2c53445b5e Remove LNTXbot files (#1921) 2023-09-11 12:35:56 +02:00
dni ⚡andGitHub 62435b3723 [TEST] workflows, added 10s timeout to lncli subprocess and logging (#1910)
* [FIX] workflows

testing why workflwos unstable instable

* debug run_command

* add timeout to process run

* log lncli output

* try catch json exception in clnrest

* settle does not return a json

* add 10 minutes timeout to regtests if they ever get stuck

* better subprocess handling

* fix race condition

* test 3

* remove

* run nr 4

* increase waits for race conditions

* revert clnrest

* better fiat amount test
2023-08-30 11:01:32 +01:00
dni ⚡andGitHub 6efe1a156b [FEAT] add codecov token (#1908)
for better coverage reports
2023-08-28 13:49:33 +02:00
dni ⚡andGitHub 85b7591f6e update to version 0.10.10 (#1773) 2023-08-28 12:40:02 +01:00
callebtcandGitHub 172c277c2d fakewallet: set tag of description_hash invoice correctly (#1901) 2023-08-28 11:02:58 +01:00
jackstar12andGitHub 2623e9247a track fiat value of payments (#1789)
* save fiat_amounts on payment creation

* show fiat amount in frontend

* add lnbits_default_accounting_currency

* extract fiat calculation logic into service

* move all currency conversions to calc_fiat_amounts

move all conversion logic into create_invoice so extensions can benefit aswell

* show user-defined and wallet-defined currency in frontend

* remove sat from fiat units

* make bundle

* improve tests

* debug log
2023-08-28 11:00:59 +01:00
7a37e72915 [REFACTOR] replace async_wrap with run_sync (#1858)
* replace async_wrap with run_sync

formatting

remove unused fn

properly raise exception, not for timeout though

* [TEST] proper startup and shutdown (#1860)

* add proper startup / shutdown in tests

* fix event loop issues

because uvloop was installed in server.py which is not the main entry point when tests are ran.
this caused the loops referenced by the locks and queues to be a different one than the one used to run the tests
(only an issue in python 3.9)

* give openapi more time, does not matter anyway, regtest takes way longer

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>

remove uvloop

* fix test

* dont touch pyproject

* fix: install uvloop in conftest

not using uvloop at all causes tests to take way longer

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-08-28 10:59:56 +01:00
48f25488df [CHORE] cleanup cache and exception on make test (#1899)
* [CHORE] cleanup cache and exception on `make test`

moved the invalidate_forever task to proper location and use `create_permanent_task`.
and there was a Task never awaited Exception when running tests

* imrpove expiration test

* move cache into utils

* fix issue with pytest_asyncio

s

---------

Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-08-28 10:56:59 +01:00
dni ⚡andGitHub 6077edad8f [CHORE] remove redundant mocks (#1902)
* [CHORE] rmeove redundant mocks

* remove mock dependency
2023-08-28 10:56:20 +01:00
jackstar12andGitHub e50a7fb2d1 refactor exchange rates (#1847)
* simplify and cache exchange rate

note that exir was removed as a provider

* add binance as provider

* log exception

* add test

* add blockchain.com provider
2023-08-24 11:59:57 +01:00
7343d1e0a0 [REFACTOR] proper create_invoice responses for descriptions (#1863)
* [REFACTOR] proper create_invoice responses for descriptions

return a exception per error it is description hash or unhashed_description
keep types

* Update lnbits/core/views/api.py

Co-authored-by: michael1011 <me@michael1011.at>

* Update lnbits/core/views/api.py

Co-authored-by: michael1011 <me@michael1011.at>

---------

Co-authored-by: michael1011 <me@michael1011.at>
2023-08-24 12:58:10 +02:00
dni ⚡andGitHub 1efec9bb80 [REFACTOR] do not throw an exception in list_parse_fallback (#1891) 2023-08-24 12:50:38 +02:00
dni ⚡andGitHub c54f48ee73 [CI] improve on github workflows (#1889)
* [CI] making a proper github CI workflow
2023-08-24 12:36:37 +02:00
jackstar12andGitHub ac0ef07a77 optimize balances view (#1779)
* optimize balances view

flake8

flake8

* fix line length
2023-08-24 11:20:15 +01:00
dni ⚡andGitHub 39d717e34c improve on openapi metadata (#1795) 2023-08-24 10:52:12 +01:00
dni ⚡andGitHub 4e6f229db2 [CHORE] string formatting default length 88 (#1887)
* [CHORE] string formatting default length 88

uses blacks default off 88 and enabled autostringformatting

* formatting

* nitpicks jackstar

fix
2023-08-24 10:26:09 +01:00
dni ⚡andGitHub 2ab18544c3 [BUG] use WalletType for get_wallet_for_key fn (#1896) 2023-08-24 08:26:34 +02:00
dni ⚡andGitHub fe88320f08 [CHORE] update mypy and new issues (#1892)
* [CHORE] update mypy and new issues

lnbits/cache.py:21: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/extension_manager.py:210: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/db.py:110: error: Only instance methods can be decorated with @property  [misc]
lnbits/tasks.py:152: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/tasks.py:171: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/core/services.py:520: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/app.py:520: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/app.py:525: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
lnbits/app.py:532: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]

* fix db.py
* fix mypy notes, type were not needed
2023-08-23 21:24:54 +02:00
dni ⚡andGitHub b0ad47b69b [CHORE] update pyln-client (#1894)
lock versions

fixup
2023-08-23 12:51:40 +02:00
dni ⚡andGitHub 88c07acc2c [FEAT] save fiat amount in api_create_invoice (#1869)
save user input into extra when he chooses a currency for api_create invoice
2023-08-23 12:45:19 +02:00
c4da1dfdce [REFACTOR] WalletType into enum (#1888)
* [REFACTOR] WalletType into enum

- move wallettype models from decorators.py into models.py
- use enum instead of int
- use HTTPStatus for consistency

* Update lnbits/core/models.py

Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>

* use dataclass

---------

Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>
2023-08-23 12:41:22 +02:00
3a653630f1 Wallets: add cln-rest (#1775)
* receive and pay works

* fix linter issues

* import Paymentstatus from core.models

* fix test real payment

* fix get_payment_status check in lnbits

* fix tests?

* simplify

* refactor AsyncClient

* inline import of get_wallet_class

fixes the previous cyclic import

* invoice stream working

* add notes as a reminder to get rid of labels when cln-rest supports payment_hash

* create Payment dummy classmethod

* remove unnecessary fields from dummy

* fixes tests?

* fix model

* fix cln bug (#1814)

* auth header

* rename cln to corelightning

* add clnrest to admin_ui

* add to clnrest allowed sources

* add allowed sources to .env.example

* allow macaroon files

* add corelightning rest to workflow

* proper env names

* cleanup routine

* log wallet connection errors and fix macaroon clnrest

* print error on connection fails

* clnrest: handle disconnects faster

* fix test use of get_payment_status

* make format

* clnrest: add unhashed_description

* add unhashed_description to test

* description_hash test

* unhashed_description not supported by clnrest

* fix checking_id return in api_payments_create_invoice

* refactor test to use client instead of api_payments

* formatting, some errorlogging

* fix test 1

* fix other tests, paid statuses was missing

* error handling

* revert unnecessary changes (#1854)

* apply review of motorina0

---------

Co-authored-by: jackstar12 <jkranawetter05@gmail.com>
Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
dni ⚡andGitHub bb5033d225 [FEAT] add lnbits-cli for commands.py (#1890)
- add superuser command for print
- add delete-settings command
2023-08-21 16:29:13 +02:00
dni ⚡andGitHub eb0b06f98c [DEV] add ruff for linting and combine linters into one workflow (#1875)
* remove unused and update black + precommit
* makefile add ruff remove unused
* F541 fix
* complete ruff ignore
* remove unused workflow and combine linters into one
add lnbits/static to ruff ignore
save preview and linelength for later
define target version for black
* ignore upgrades for mypy
* remove flake8
* ignore F401 for __init__ files
* unused pylint comment
* unused noqa
* ignore upgrades for black
* run linting on py3.9 and py3.10
* dont assume python
2023-08-19 07:38:04 +02:00
jackstar12andGitHub 642f9a4c00 Update payments on invoice creation (#1782)
* update payments on invoice creation
* remove unnecessary payment polling
2023-08-18 12:10:52 +02:00
jackstar12andGitHub 8a6e411a0d [REFACTOR] payments sse endpoint (#1781)
* exclude sse from gzip

* refactor sse endpoint

* cleanup imports
2023-08-18 11:05:14 +01:00
stackregisterandGitHub d4de78f1e8 Add description to POST v1/payments endpoint. (#1848) 2023-08-18 10:30:45 +01:00
1fd4d9d514 cancel all long-running tasks (#1793)
* add centralized task management

in order to properly cleanup all long-running tasks we have to keep a list of them

* use new task management functions

* unify shutdown events

* vlads suggestions

rename variable for create_task
wrap cancel() with try/catch

fixup

* rename func to coro

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-08-18 10:25:33 +01:00
dni ⚡andGitHub 65db43ace4 feat: catch wallet init exception, string formatting (#1846)
this catches exception on set_wallet_class and switches to VoidWallet if it fails
2023-08-18 10:23:57 +01:00
dni ⚡andGitHub 59acd3a2ef [REFACTOR] cleanup views/api.py (#1865)
pull out models
line lengths
change models for tests
2023-08-18 10:22:22 +01:00
dni ⚡andGitHub 905afc1f5c FEAT: add installed extensions to log (#1857)
for immediate vision of installed extensions
2023-08-18 10:13:37 +01:00
dni ⚡andGitHub c7cf743852 [TEST] run tests.yml on py39 and py310 (#1882)
i think it is a good idea to test both at least for fakewallet tests.
2023-08-18 09:50:56 +01:00
b43e7efa0b [FEATURE] Adding coverage for 'lntbs' signet prefix (#1881)
* Update bolt11-decoder.js

Adding coverage for 'lntbs' signet prefix

* adding 'make bundle' modified files

---------

Co-authored-by: enaples <enaples@users.noreply.github.com>
2023-08-18 09:48:45 +01:00
dni ⚡andGitHub c8414829d7 [TEST] dont install default extensions for tests (#1861) 2023-08-16 19:19:21 +02:00
dni ⚡andGitHub 5e03f8eed8 [TEST] update pytest-cov to remove warning (#1867)
cleanup this warning when running tests
2023-08-16 16:26:48 +02:00
dni ⚡andGitHub 511a0fd895 [CHORE] cln.py pyln-client is not optional (#1859)
* [refactor] pyln-client is not optional

some time ago we added pyln do main dependencies, its need for think like, saas or docker.
so this import was dead code

* stubs for pyln.client
2023-08-16 15:54:06 +02:00
dni ⚡andGitHub 2434518d21 [TEST] better logs (#1866)
now you can see which lnbits logs belongs to which tests! :)
2023-08-16 12:31:56 +01:00
dni ⚡andGitHub 26a0633d49 [CHORE] fix pyright warning in db.py (#1868)
```warning: Class methods should take a "cls" parameter (reportSelfClsParameterName)```
2023-08-16 12:41:49 +02:00
dni ⚡andGitHub f0a66e41fb [CHORE] flake8 issues E402, E721 and F821 (#1874)
* F821: undefine name
disabled and logged webhook_listener for opennode and lnpay because they are obviously not working
* E402: module level import not at top of file
* E721 fixes, only popped up for python3.9 not 3.10
2023-08-16 12:22:14 +02:00
dni ⚡andGitHub 355806608b [CHORE] E722 bare exception fix (#1871)
* [CHORE] E722 bare exception fix
remove all bare exceptions from codebase and change it in `.flake8`
2023-08-16 12:17:54 +02:00
jackstar12andGitHub 3aef1605be Fix csv export (#1852)
* fetch all payments for csv export

* include fiat amount and currency in export
2023-08-15 12:39:12 +01:00
dni ⚡andGitHub f096b51f70 [TEST] proper credit_wallet function from services (#1862) 2023-08-07 21:49:37 +02:00
michael1011andGitHub 03acf5e5ec UI: hide paste payment request in unsupported environments (#1853) 2023-08-03 20:38:05 +02:00
772ae2a1ab support uvicorn reload functionality (#1841)
* support uvicorn reload functionality
see:
https://github.com/encode/uvicorn/blob/8239373ec649b1a41179070b6ba2c8dc549df63e/uvicorn/main.py#L580-L582
* add docs for --reload

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-08-03 09:53:36 +02:00
jackstar12andGitHub 2577ce7f81 add cache utility (#1790)
* add simple caching utility

* test cache

* remove prefix, default on get

* check expiry in pop aswell

* remove unnecessary type

* improve invalidation task

increase default interval to 10 seconds - doesnt have to check that often.
instead of recreating the dict everytime mutate the existing one
2023-08-02 14:13:31 +02:00
callebtcandGitHub 9bc8a9db55 UI: paste payment request from clipboard (#1845)
* paste payment request from clipboard

* remove unelevated
2023-08-02 12:42:23 +01:00
callebtcandGitHub 9f40b3cdbd Tests: hold invoice tests pending during payment (#1839)
* tests: hold invoice tests pending during payment

* sigh...

* ok

* hash preimage

* preimage_hash == payment_hash

* should error

* check that payment is gone

* cancel task while in-flight

* lnbits endpoing return error

* return error

* add both options

* fix list

* respond to error

* return false if payment status errors

* outgoing invoice should be deleted after one status check

* test api_payments_create error
2023-07-31 20:25:22 +02:00
jackstar12andGitHub 1ab81f6d7e Bug: invalid type converter (#1842)
* temporary test case

* remove buggy converter
2023-07-31 10:21:45 +02:00
jackstar12andGitHub 67d3a4f359 remove nested filters (#1843)
* remove nested filters

* remove from openapi schema aswell
2023-07-31 10:21:30 +02:00
7f0c7138af TEST: add hold invoices to test helpers and a basic test (#1838)
* add hold invoices and basic test

* run request in parallel

* create_task

* dont cancel it

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-07-26 14:18:14 +02:00
jackstar12andGitHub dda6c1b3c1 websocket internal payment notifications (#1831)
* add send_payment_notification service
payment notifications are sent from multiple places with inconsistent and incomplete data
* adopt new send_payment_notification service
* add tests
2023-07-26 12:08:22 +02:00
Mariusz KogenandGitHub cf0a87582c Ads layout fix (#1835) 2023-07-26 10:38:45 +02:00
4b25714b31 Fix drawer (#1836)
* make sidebar above tabs on mobile
* Can we add padding instead
* fix: add padding for small screen
* make bundle... again

---------

Co-authored-by: Ben <ben@arc.wales>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2023-07-26 10:35:52 +02:00
Mariusz KogenandGitHub 0ff0abf442 Adding Start9 (#1834) 2023-07-25 15:38:54 +02:00
dni ⚡andGitHub 6ce0387e1c FEAT: proper failing invoice/payment responses for VoidWallet (#1805)
before that change anyone using the api just got a 500 error without any error message.
there was also a bug stacetrace in the logs each time.

flake8
2023-07-25 14:13:34 +01:00
dni ⚡andGitHub 427f155c76 follow redirect for lnurls (#1832)
closes #976
2023-07-25 11:06:37 +02:00
dni ⚡andGitHub a81a2b5a08 fix ugly printed error of uvicorn.error (#1833) 2023-07-24 13:25:47 +02:00
callebtcandGitHub 11fec7a889 [Wallets] CLN: fix pending state check (#1770)
* better checking

* flake8 fix

* make format

* invoices scope are function for tests

* invoice back to sessionbut keep real_invoice for now

* make format

* comment

* get payment by checking id and test
2023-07-24 12:00:41 +02:00
dni ⚡andGitHub f478c1848a update embit to 0.7.0 (#1777) 2023-07-21 10:42:35 +02:00
callebtcandGitHub 2d4c9f349b Wallets: Catch errors during cleanup (#1829)
* jetz aba

* catch RuntimeError
2023-07-21 09:50:50 +02:00
Tiago VasconcelosandGitHub befb288900 Fix admin notifications (#1803)
* fix save badge being on all the time

* make deep copy and add restart to notification popup
2023-07-21 09:50:18 +02:00
dni ⚡andGitHub 963fd5de50 add github workflow for checking openapi (#1551)
* add github workflow for checking openapi

* verbose errors

* actual fix? remove env_names from BaseSettings class?

* dont kill process in workflow

* poetry version 1.5.1 for openapiu
2023-07-19 12:28:27 +01:00
ArcandGitHub a3aafe4b6f Data returned from wallet websocket was not json (#1823)
* Fixed walet websocket json

* format
2023-07-19 12:14:03 +01:00
f384c5c366 Increase default invoice expiry to 1 hour (#1825)
* more reasonable default expiry

600 = 10 minutes which is a pretty short amount of time, for example when using lnbits fundingsource inside lnbits itself it would fail if you do a submarine swap or anything involved with hold invoices

i think 3600 = 1 hour should not make a difference in performance and is a way more reasonable expiry

* change default in .env.example

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-07-19 13:13:46 +02:00
dni ⚡andGitHub 153e22fb54 remove extensions dir completly, create it on startup (#1778)
* remove extensions dir compleltly, and create it on server.py start

* also ignore symlinks
2023-07-18 13:46:50 +02:00
dni ⚡andGitHub 213be79aaa quickfix for failing checks (#1819)
root package is needed for tests

root package is needed for mypy

removing testfile
2023-07-11 13:25:35 +02:00
dni ⚡andGitHub f7cbe74d77 Update poetry to 1.5.1 (#1804)
* update poetry to 1.5.1 inside workflows

* update lock file to poetry 1.5.1

* test run 1

* test run 2

* run 3

* try different sortorder

* cli should use --no-root

here  are the refereneces.
https://python-poetry.org/docs/basic-usage/#installing-dependencies-only
https://pip.pypa.io/en/stable/topics/local-project-installs/#regular-installs

* cache migrations aswell

* test flake

* optimize docker image for regtest building

* playng

* remove test run 1

* remove run 2

* remove run 3

* run 4

* run 6

* run 7

* run 8

* run 9

* run 10
2023-07-11 12:46:40 +02:00
ArcandGitHub d0d5c4d033 Update README.md (#1818) 2023-07-11 09:34:21 +01:00
ArcandGitHub 0adaf03de5 update favicon (#1811) 2023-07-07 10:21:24 +01:00
83a04d8f74 CI adjustment - Pinkify Legend (#1766)
Add files via upload
Update base.scss
* making the bundle for pinkify
fix flamingo

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-07-05 14:39:39 +02:00
dni ⚡andGitHub e13426a56b needs restart after saving killswitch (#1800)
fix prettier
2023-07-03 16:52:33 +02:00
6aaa46e5a0 Fix ip blocker 2nd try (#1796)
* fix ipblock v2

fix ipblock 2nd try

* remove sleep inside ip block

dont ignore other exceptions

typo

* remove asyncio

* Update lnbits/middleware.py

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2023-07-03 11:25:00 +02:00
callebtcandGitHub b9db3eafe0 remove sleep (#1797) 2023-07-03 11:12:09 +02:00
dni ⚡andGitHub ae9eb314cc fix bundle for 0.10.9 (#1792) 2023-07-03 10:29:03 +02:00
dni ⚡andGitHub fe393b83ab ip blocker only call next after validation (#1791)
* only call next after validation

* lets the blocked ips wait :)

* calles suggestion :)
2023-07-03 10:26:35 +02:00
jackstar12andGitHub bc55d52ea2 Refactor get walletclass (#1776)
* move `get_wallet_class` to wallets module

* adjust imports, fix type issues

flake8
2023-06-27 16:11:00 +02:00
dni ⚡andGitHub 9a0878de19 ignore upgrades dir in linters and formatters and precommit (#1783) 2023-06-26 10:38:40 +02:00
callebtcandGitHub be7c589722 add pre-commit-config (#1785) 2023-06-26 10:38:16 +02:00
dni ⚡andGitHub 28a55b2d41 precommit only check prettier if html/css/js/json (#1786)
add precommit checks only for types
2023-06-26 10:35:39 +02:00
callebtcandGitHub d66c83c91a no timeout for the sse connection (#1784) 2023-06-26 10:02:49 +02:00
7e1f43933d Adds security tools, such as a rate limiter, IP block/allow, server logs (#1606)
* added ratelimiter
* Adds server logs to admin ui
* Added IP allow/ban list
* fixed remove ips
* Split rate limit number and unit
* security tab and background tasks for killswitch
* fix test for auditor api

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-06-20 11:26:33 +02:00
Tiago VasconcelosandGitHub 758a4ecaf6 Fix user label across lnbits (#1771)
* check for super user on get_user
* remove unecessary assingments
2023-06-20 08:41:57 +02:00
callebtcandGitHub 339645a912 docs: add precommit hooks (#1772)
* docs: add precommit hooks
* adjust test
2023-06-20 08:41:23 +02:00
9014e73529 Create cn.js (#1768)
* Create cn.js
Add chinese translation
* added additional files for chinese language
* Update lnbits/templates/base.html

---------

Co-authored-by: hodlbaby <52352588+hodlbaby@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2023-06-20 08:41:04 +02:00
48fc73d9d4 Ability to limit available fiat currencies (#1748)
* feat: limit number of available fiat currencies
* feat: Add allowed currency selector to Admin UI
* motorinas suggestions

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-06-19 12:24:19 +02:00
OrenandGitHub 150a33009d s/gihub/github/g (#1767) 2023-06-19 12:22:53 +02:00
jackstar12andGitHub e6499104c0 Wallets refactor (#1729)
* feat: cleanup function for wallet

* update eclair implementation

* update lnd implementation

* update lnbits implementation

* update lnpay implementation

* update lnbits implementation

* update opennode implementation

* update spark implementation

* use base_url for clients

* fix lnpay

* fix opennode

* fix lntips

* test real invoice creation

* add small delay to test

* test paid invoice stream

* fix lnbits

* fix lndrest

* fix spark

fix spark

* check node balance in test

* increase balance check delay

* check balance in pay test aswell

* make sure get_payment_status is called

* fix lndrest

* revert unnecessary changes
2023-06-19 12:12:00 +02:00
Vlad StanandGitHub 49411e58cc Fix: extension links (#1764)
* fix: open extension link
* fix: do not show `Enable` for admin extensions to non-admin users
2023-06-15 19:06:21 +02:00
Vlad StanandGitHub 8c0e7725de Add option to drop extension db at un-install time or later (#1746)
* chore: remove un-used file
* feat: allow extension DB clean-up
* feat: i18n and bundle update
* chore: code format
* fix: button color
* chore: delete temp file
* chore: fix merge conflicts
* chore: add extra log
* chore: bump CACHE_VERSION to `37`
2023-06-15 16:22:18 +02:00
Vlad StanandGitHub 95281eba8c feat: show extension repo as tooltip for each repo (#1743) 2023-06-15 16:08:24 +02:00
ArcandGitHub cd17b59a3a Change asdict to dict (#1757) 2023-06-15 15:44:44 +02:00
dni ⚡andGitHub ec623f4861 remove settings logging (security) (#1763) 2023-06-15 15:44:08 +02:00
7c479a3ebf Hide the superuser ID URL from logs with comment where to find superuser ID (#1749)
* Hide the superuser ID URL from logs with comment where to find admin ID
* Update admin_ui.md

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-06-15 15:34:39 +02:00
dni ⚡andGitHub 9017cee491 quickfix spacing und mobile footer (#1740) 2023-06-15 15:32:10 +02:00
dni ⚡andGitHub 2940716807 bump to version 0.10.9 (#1742) 2023-06-15 15:30:39 +02:00
dni ⚡andGitHub c48e2329c5 TEST: fix tests for real wallets (#1761)
* fix tests for real wallets
* change realinvoice provider to lnd-1
* use lnd-3 for tests
* fix eclair tests
* use bullseye in dockerimage
2023-06-15 12:36:28 +02:00
dni ⚡andGitHub 8ed2422ade highlight active language #1730 (#1741) 2023-06-01 11:33:53 +01:00
Vlad StanandGitHub af660fec14 fix: ext short description for pre-installed extensions (#1744)
* fix: update `installed_extensions` entry after the ext has been unzipped

* fix: show installedRelease description as fallback
2023-05-26 10:48:40 +01:00
ArcandGitHub 6bbebe0cee Fixes and expands payments table csv (#1739) 2023-05-25 05:29:15 +01:00
ArcandGitHub f863a043cb Adds translations for send/receive (#1738)
* Send/receive translation

* bundle fundle
2023-05-24 20:59:34 +01:00
ArcandGitHub 2216649412 remove msat fee to sat (#1737)
* remove msat fee

* Fixed some formatting
2023-05-24 20:27:56 +01:00
ArcandGitHub 9d8c78c927 added msats (#1736) 2023-05-24 19:47:54 +01:00
ArcandGitHub 02bf2f9819 Better wallet format (#1735)
* Fixing big inout

* btter wallet format
2023-05-24 15:44:28 +01:00
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
245 changed files with 51571 additions and 7112 deletions
+14
View File
@@ -0,0 +1,14 @@
# Top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file, utf-8 charset
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[/lnbits/static/vendor/*]
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
+169 -60
View File
@@ -1,57 +1,17 @@
#For more information on .env files, their content and format: https://pypi.org/project/python-dotenv/
HOST=127.0.0.1
PORT=5000
######################################
########### Admin Settings ###########
######################################
# uvicorn variable, uncomment to allow https behind a proxy
# FORWARDED_ALLOW_IPS="*"
DEBUG=false
# Allow users and admins by user IDs (comma separated list)
LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
# Warning: Enabling this will make LNbits ignore this configuration file. Your settings will
# be stored in your database and you will be able to change them only through the Admin UI.
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available.
# Warning: Enabling this will make LNbits ignore most configurations in file. Only the
# configurations defined in `ReadOnlySettings` will still be read from the environment variables.
# The rest of the settings will be stored in your database and you will be able to change them
# only through the Admin UI.
# Disable this to make LNbits use this config file again.
LNBITS_ADMIN_UI=false
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Ad space description
# LNBITS_AD_SPACE_TITLE="Supported by"
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png"
# Hides wallet api, extensions can choose to honor
LNBITS_HIDE_API=false
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
# The extension must be removed from this list in order to not be re-installed.
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
# for both PostgreSQL and CockroachDB, you'll need to install
# psycopg2 as an additional dependency
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
LNBITS_SERVICE_FEE="0.0"
# value in millisats
LNBITS_RESERVE_FEE_MIN=2000
# value in percent
LNBITS_RESERVE_FEE_PERCENT=1.0
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
@@ -60,15 +20,22 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
# Choose from LNPayWallet, OpenNodeWallet, ClicheWallet,
# LndWallet, LndRestWallet, CoreLightningWallet, EclairWallet,
# LnTipsWallet, LNbitsWallet, SparkWallet, FakeWallet,
HOST=127.0.0.1
PORT=5000
######################################
########## Funding Source ############
######################################
# which fundingsources are allowed in the admin ui
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, AlbyWallet, ZBDWallet, OpenNodeWallet"
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
# just so you can see the UI before dealing with this file.
# Invoice expiry for LND, CLN, Eclair, LNbits funding sources
LIGHTNING_INVOICE_EXPIRY=600
LIGHTNING_INVOICE_EXPIRY=3600
# Set one of these blocks depending on the wallet kind you chose above:
@@ -82,6 +49,11 @@ SPARK_TOKEN=myaccesstoken
# CoreLightningWallet
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
# CoreLightningRestWallet
CORELIGHTNING_REST_URL=http://127.0.0.1:8185/
CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon" # or BASE64/HEXSTRING
CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert"
# LnbitsWallet
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY
@@ -89,15 +61,15 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
# LndWallet
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=10009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
LND_GRPC_CERT="/home/bob/.lnd/tls.cert"
LND_GRPC_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING
# To use an AES-encrypted macaroon, set
# LND_GRPC_MACAROON="eNcRyPtEdMaCaRoOn"
# LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
LND_REST_CERT="/home/bob/.lnd/tls.cert"
LND_REST_MACAROON="/home/bob/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # or HEXSTRING
# To use an AES-encrypted macaroon, set
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
@@ -108,6 +80,14 @@ LNPAY_API_KEY=LNPAY_API_KEY
# Wallet Admin in Wallet Access Keys
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
# AlbyWallet
ALBY_API_ENDPOINT=https://api.getalby.com/
ALBY_ACCESS_TOKEN=ALBY_ACCESS_TOKEN
# ZBDWallet
ZBD_API_ENDPOINT=https://api.zebedee.io/v0/
ZBD_API_KEY=ZBD_ACCESS_TOKEN
# OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY
@@ -125,7 +105,136 @@ ECLAIR_PASS=eclairpw
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
LNTIPS_API_ENDPOINT=https://ln.tips
# Cashu Mint
# Use a long-enough random (!) private key.
# Once set, you cannot change this key as for now.
CASHU_PRIVATE_KEY="SuperSecretPrivateKey"
######################################
####### Auth Configurations ##########
######################################
# Secret Key: will default to the hash of the super user. It is strongly recommended that you set your own value.
AUTH_SECRET_KEY=""
AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, google-auth, github-auth, keycloak-auth
AUTH_ALLOWED_METHODS="user-id-only, username-password"
# Set this flag if HTTP is used for OAuth
# OAUTHLIB_INSECURE_TRANSPORT="1"
# Google OAuth Config
# Make sure that the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# GitHub OAuth Config
# Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/github/token
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Keycloak OAuth Config
# Make sure that the valid redirect URIs contain https://{domain}/api/v1/auth/keycloak/token
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_DISCOVERY_URL=""
######################################
# uvicorn variable, uncomment to allow https behind a proxy
# FORWARDED_ALLOW_IPS="*"
# Server security, rate limiting ips, blocked ips, allowed ips
LNBITS_RATE_LIMIT_NO="200"
LNBITS_RATE_LIMIT_UNIT="minute"
LNBITS_ALLOWED_IPS=""
LNBITS_BLOCKED_IPS=""
# Allow users and admins by user IDs (comma separated list)
# if set new users will not be able to create accounts
LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# ID of the super user. The user ID must exist.
# SUPER_USER=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Start LNbits core only. The extensions are not loaded.
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
# Disable account creation for new users
# LNBITS_ALLOW_NEW_ACCOUNTS=false
# Enable Node Management without activating the LNBITS Admin GUI
# by setting the following variables to true.
LNBITS_NODE_UI=false
LNBITS_PUBLIC_NODE_UI=false
# Enabling the transactions tab can cause crashes on large Core Lightning nodes.
LNBITS_NODE_UI_TRANSACTIONS=false
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Ad space description
# LNBITS_AD_SPACE_TITLE="Supported by"
# csv ad space, format "<url>;<img-light>;<img-dark>, <url>;<img-light>;<img-dark>", extensions can choose to honor
# LNBITS_AD_SPACE="https://shop.lnbits.com/;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-light.png;https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits/static/images/lnbits-shop-dark.png"
# Hides wallet api, extensions can choose to honor
LNBITS_HIDE_API=false
# LNBITS_EXTENSIONS_MANIFESTS="https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json,https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions-trial.json"
# GitHub has rate-limits for its APIs. The limit can be increased specifying a GITHUB_TOKEN
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# Path where extensions will be installed (defaults to `./lnbits/`).
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
# Extensions to be installed by default. If an extension from this list is uninstalled then it will be re-installed on the next restart.
# The extension must be removed from this list in order to not be re-installed.
LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
# for both PostgreSQL and CockroachDB, you'll need to install
# psycopg2 as an additional dependency
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
# the service fee (in percent)
LNBITS_SERVICE_FEE=0.0
# the wallet where fees go to
# LNBITS_SERVICE_FEE_WALLET=
# the maximum fee per transaction (in satoshis)
# LNBITS_SERVICE_FEE_MAX=1000
# disable fees for internal transactions
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
# value in millisats
LNBITS_RESERVE_FEE_MIN=2000
# value in percent
LNBITS_RESERVE_FEE_PERCENT=1.0
# limit the maximum balance for each wallet
# throw an error if the wallet attempts to create a new invoice
# LNBITS_WALLET_LIMIT_MAX_BALANCE=1000000
# LNBITS_WALLET_LIMIT_DAILY_MAX_WITHDRAW=1000000
# LNBITS_WALLET_LIMIT_SECS_BETWEEN_TRANS=60
# Limit fiat currencies allowed to see in UI
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
######################################
###### Logging and Development #######
######################################
DEBUG=false
DEBUG_DATABASE=false
BUNDLE_ASSETS=true
# logging into LNBITS_DATA_FOLDER/logs/
ENABLE_LOG_TO_FILE=true
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
LOG_ROTATION="100 MB"
LOG_RETENTION="3 months"
# for database cleanup commands
# CLEANUP_WALLETS_DAYS=90
-16
View File
@@ -1,16 +0,0 @@
[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: ''
---
+67
View File
@@ -0,0 +1,67 @@
name: prepare
inputs:
python-version:
description: "Python Version"
required: true
default: "3.9"
poetry-version:
description: "Poetry Version"
default: "1.7.0"
node-version:
description: "Node Version"
default: "18.x"
npm:
description: "use npm"
default: false
type: boolean
runs:
using: "composite"
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
# cache poetry install via pip
cache: "pip"
- name: Set up Poetry ${{ inputs.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ inputs.poetry-version }}
- name: Setup a local virtual environment (if no poetry.toml file)
shell: bash
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies
shell: bash
run: poetry install
- name: Use Node.js ${{ inputs.node-version }}
if: ${{ (inputs.npm == 'true') }}
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
- uses: actions/cache@v3
if: ${{ (inputs.npm == 'true') }}
name: Define a cache for the npm based on the dependencies lock file
with:
path: ./node_modules
key: npm-${{ hashFiles('package-lock.json') }}
- name: Install npm packages
if: ${{ (inputs.npm == 'true') }}
shell: bash
run: npm install
+60
View File
@@ -0,0 +1,60 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
jobs:
lint:
uses: ./.github/workflows/lint.yml
tests:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.9", "3.10"]
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml
with:
python-version: ${{ matrix.python-version }}
migrations:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.9"]
uses: ./.github/workflows/tests.yml
with:
make: test-migration
db-name: migration
openapi:
needs: [ lint ]
uses: ./.github/workflows/make.yml
with:
make: openapi
regtest:
needs: [ lint ]
uses: ./.github/workflows/regtest.yml
strategy:
matrix:
python-version: ["3.9"]
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
with:
python-version: ${{ matrix.python-version }}
backend-wallet-class: ${{ matrix.backend-wallet-class }}
jmeter:
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.5.1"]
uses: ./.github/workflows/jmeter.yml
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }}
+2 -2
View File
@@ -2,9 +2,9 @@ name: codeql
on:
push:
branches: [main, ]
branches: [main, dev]
pull_request:
branches: [main]
branches: [main, dev]
schedule:
- cron: '0 12 * * 5'
+58
View File
@@ -0,0 +1,58 @@
name: docker
on:
workflow_dispatch:
inputs:
tag:
default: latest
type: string
workflow_call:
inputs:
tag:
default: latest
type: string
jobs:
push_to_dockerhub:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Build and push latest tag
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits:latest
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
-27
View File
@@ -1,27 +0,0 @@
name: flake8
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
- name: Run tests
run: make flake8
-33
View File
@@ -1,33 +0,0 @@
name: formatting
on: [push, pull_request]
jobs:
checks:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install packages
run: |
poetry install
npm install prettier
- name: Check black
run: make checkblack
- name: Check isort
run: make checkisort
- uses: actions/setup-node@v3
- name: Check prettier
run: make checkprettier
+98
View File
@@ -0,0 +1,98 @@
name: JMeter Tests
on:
workflow_call:
inputs:
python-version:
description: "Python Version"
required: true
default: "3.9"
type: string
poetry-version:
description: "Poetry Version"
required: true
default: "1.5.1"
type: string
jobs:
action_build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
cache: "pip"
- name: Set up Poetry ${{ inputs.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ inputs.poetry-version }}
- name: create logs and reports dir
run: |
mkdir logs
mkdir reports
- name: install packages
run: poetry install
- name: run LNbits
env:
LNBITS_ADMIN_UI: true
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
run: |
poetry run lnbits > logs/lnbits.log &
sleep 5
- name: install jmeter
run: |
java -version
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.2.zip
unzip apache-jmeter-5.6.2.zip
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -v
- name: start mirror server
run: |
libs_dir=$GITHUB_WORKSPACE/apache-jmeter-5.6.2/lib/
echo "Fix bad Jmeter lib names. Lib dir: $libs_dir"
mv $libs_dir/slf4j-api-1.7.36.jar $libs_dir/slf4j-api-1.7.25.jar
mv $libs_dir/log4j-slf4j-impl-2.20.0.jar $libs_dir/log4j-slf4j-impl-2.11.0.jar
mv $libs_dir/log4j-api-2.20.0.jar $libs_dir/log4j-api-2.11.1.jar
mv $libs_dir/log4j-core-2.20.0.jar $libs_dir/log4j-core-2.11.1.jar
mv $libs_dir/log4j-1.2-api-2.20.0.jar $libs_dir/og4j-1.2-api-2.11.1.jar
echo "Starting the mirror server on dfault port 8081."
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/mirror-server &
- name: run jmx scripts
run: |
for file in $( ls $GITHUB_WORKSPACE/integration/*.jmx); do
echo "Running test with $file"
filename=$(basename "$file" ".jmx")
$GITHUB_WORKSPACE/apache-jmeter-5.6.2/bin/jmeter -n -t $file -l logs/$filename.log -e -o reports ;
error_count=$(cat jmeter.log | grep "summary =" | awk '{print $19}')
echo "Error count: $error_count"
if [[ "$error_count" == "0" ]]; then
echo "Test $filename OK."
rm -r reports/*
else
echo "Test $filename failed. Error count: $error_count."
exit 1
fi
done
- uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: jmeter-test-results
path: |
reports/
logs/
+55
View File
@@ -0,0 +1,55 @@
name: lint
on:
workflow_call:
jobs:
black:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.9", "3.10"]
with:
make: checkblack
python-version: ${{ matrix.python-version }}
ruff:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.9", "3.10"]
with:
make: checkruff
python-version: ${{ matrix.python-version }}
mypy:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.9", "3.10"]
with:
make: mypy
python-version: ${{ matrix.python-version }}
pyright:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.9", "3.10"]
with:
make: pyright
python-version: ${{ matrix.python-version }}
npm: true
prettier:
uses: ./.github/workflows/make.yml
with:
make: checkprettier
npm: true
bundle:
uses: ./.github/workflows/make.yml
with:
make: checkbundle
npm: true
+34
View File
@@ -0,0 +1,34 @@
name: make
on:
workflow_call:
inputs:
make:
description: "make command that is run"
required: true
type: string
npm:
description: "use npm install"
default: false
type: boolean
python-version:
description: "python version"
type: string
default: "3.10"
jobs:
make:
name: ${{ inputs.make }} (${{ inputs.python-version }})
strategy:
matrix:
os-version: ["ubuntu-latest"]
node-version: ["18.x"]
runs-on: ${{ matrix.os-version }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/prepare
with:
python-version: ${{ inputs.python-version }}
node-version: ${{ matrix.node-version }}
npm: ${{ inputs.npm }}
- run: make ${{ inputs.make }}
-43
View File
@@ -1,43 +0,0 @@
name: migrations
on: [pull_request]
jobs:
sqlite-to-postgres:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: lnbits
POSTGRES_PASSWORD: lnbits
POSTGRES_DB: migration
ports:
# maps tcp port 5432 on service container to the host
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
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: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install dependencies
run: |
poetry install
sudo apt install unzip
- name: Run migrations
run: |
make test-migration
-27
View File
@@ -1,27 +0,0 @@
name: mypy
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
- name: Run tests
run: make mypy
+41
View File
@@ -0,0 +1,41 @@
name: LNbits CI / nix
# - run : on main, dev, nix and cachix branches when relevant files change
# - cache : on main, dev and cachix branches when relevant files change
on:
push:
branches:
- main
- dev
- nix
- cachix
paths:
- 'flake.nix'
- 'flake.lock'
- 'pyproject.toml'
- 'poetry.lock'
- '.github/workflows/nix.yml'
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'pyproject.toml'
- 'poetry.lock'
jobs:
nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v24
with:
nix_path: nixpkgs=channel:nixos-23.11
- uses: cachix/cachix-action@v13
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix'
with:
name: lnbits
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix build -L
- run: cachix push lnbits ./result
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/cachix'
-69
View File
@@ -1,69 +0,0 @@
name: Build and push Docker image on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-*"
jobs:
build:
runs-on: ubuntu-20.04
name: Build and push lnbits image
steps:
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v3
- name: Import environment variables
id: import-env
shell: bash
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Show available Docker buildx platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \
--output "type=registry" ./
- name: Run Docker buildx against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \
--output "type=registry" ./
-27
View File
@@ -1,27 +0,0 @@
name: pylint
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
- name: Run tests
run: make pylint
-28
View File
@@ -1,28 +0,0 @@
name: pyright
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
npm install
- name: Run tests
run: make pyright
+54 -196
View File
@@ -1,223 +1,81 @@
name: regtest
on: [push, pull_request]
on:
workflow_call:
inputs:
make:
default: test
type: string
python-version:
default: "3.9"
type: string
os-version:
default: "ubuntu-latest"
type: string
backend-wallet-class:
required: true
type: string
jobs:
LndRestWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
regtest:
runs-on: ${{ inputs.os-version }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
- name: Set up Docker Buildx
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
uses: docker/setup-buildx-action@v2
- name: Build and push
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
uses: docker/build-push-action@v4
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
context: .
push: false
tags: lnbits/lnbits:latest
cache-from: type=registry,ref=lnbits/lnbits:latest
cache-to: type=inline
- name: Setup Regtest
run: |
docker build -t lnbitsdocker/lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry install
- name: Run tests
- uses: ./.github/actions/prepare
with:
python-version: ${{ inputs.python-version }}
- name: Create fake admin
if: ${{ inputs.backend-wallet-class == 'LNbitsWallet' }}
run: docker exec lnbits-lnbits-1 poetry run python tools/create_fake_admin.py
- name: Run Tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: ./docker/data/lnd-1/tls.cert
LND_REST_MACAROON: ./docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
LndWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Regtest
run: |
docker build -t lnbitsdocker/lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry install
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LndWallet
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009
LND_GRPC_CERT: docker/data/lnd-1/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
CoreLightningWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Regtest
run: |
docker build -t lnbitsdocker/lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry install
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: CoreLightningWallet
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
LNbitsWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Regtest
run: |
docker build -t lnbitsdocker/lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
docker exec lnbits-legend-lnbits-1 /bin/bash -c "poetry run python tools/create_fake_admin.py"
- name: Install dependencies
run: |
poetry install
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LNbitsWallet
CORELIGHTNING_REST_URL: https://localhost:3001
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
EclairWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Regtest
run: |
docker build -t lnbitsdocker/lnbits-legend .
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Install dependencies
run: |
poetry install
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: EclairWallet
ECLAIR_URL: http://127.0.0.1:8082
ECLAIR_PASS: lnbits
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
run: make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
+28
View File
@@ -0,0 +1,28 @@
name: release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes --draft
docker:
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: ${{ github.ref_name }}
+35 -47
View File
@@ -1,70 +1,58 @@
name: tests
on: [push, pull_request]
on:
workflow_call:
inputs:
make:
default: test
type: string
python-version:
default: "3.9"
type: string
os-version:
default: "ubuntu-latest"
type: string
db-url:
default: ""
type: string
db-name:
default: "lnbits"
type: string
jobs:
sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
- name: Run tests
run: make test
postgres:
runs-on: ubuntu-latest
tests:
runs-on: ${{ inputs.os-version }}
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
POSTGRES_USER: lnbits
POSTGRES_PASSWORD: lnbits
POSTGRES_DB: ${{ inputs.db-name }}
ports:
# maps tcp port 5432 on service container to the host
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
- uses: ./.github/actions/prepare
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: |
poetry install
- name: Run tests
python-version: ${{ inputs.python-version }}
- name: Run Tests
env:
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
run: make test
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
run: make ${{ inputs.make }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
+6 -2
View File
@@ -23,6 +23,7 @@ tests/data/*.sqlite3
*.pyc
*.env
.env
.pre-commit-config.yaml
data
*.sqlite3
@@ -32,7 +33,10 @@ __bundle__
coverage.xml
node_modules
lnbits/static/bundle.*
lnbits/static/bundle.js
lnbits/static/bundle.css
lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
docker
# Nix
@@ -44,5 +48,5 @@ fly.toml
lnbits-backup.zip
# Ignore extensions (post installable extension PR)
extensions/
extensions
upgrades/
+30
View File
@@ -0,0 +1,30 @@
exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/extensions/.*|^lnbits/upgrades/.*|^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: 24.2.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.2
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: '50c5478ed9e10bf360335449280cf2a67f4edb7a'
hooks:
- id: prettier
types_or: [css, javascript, html, json]
args: ['lnbits']
+1
View File
@@ -6,6 +6,7 @@
*.yml
**/lnbits/extensions/*
**/lnbits/upgrades/*
**/lnbits/static/vendor
**/lnbits/static/bundle.*
+3 -4
View File
@@ -1,14 +1,14 @@
FROM python:3.10-slim
FROM python:3.10-slim-bullseye
RUN apt-get clean
RUN apt-get update
RUN apt-get install -y curl pkg-config build-essential
RUN apt-get install -y curl pkg-config build-essential libnss-myhostname
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# needed for backups postgresql-client version 14 (pg_dump)
RUN apt-get install -y wget
RUN apt-get install -y apt-utils 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
@@ -20,7 +20,6 @@ COPY . .
RUN mkdir data
RUN poetry config virtualenvs.create false
RUN poetry install --only main
ENV LNBITS_PORT="5000"
+59 -35
View File
@@ -2,9 +2,9 @@
all: format check
format: prettier isort black
format: prettier black ruff
check: mypy pyright pylint flake8 checkisort checkblack checkprettier
check: mypy pyright checkblack checkruff checkprettier checkbundle
prettier:
poetry run ./node_modules/.bin/prettier --write lnbits
@@ -12,20 +12,17 @@ prettier:
pyright:
poetry run ./node_modules/.bin/pyright
black:
poetry run black .
flake8:
poetry run flake8
mypy:
poetry run mypy
isort:
poetry run isort .
black:
poetry run black .
pylint:
poetry run pylint *.py lnbits/ tools/ tests/
ruff:
poetry run ruff check . --fix
checkruff:
poetry run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check lnbits
@@ -33,8 +30,11 @@ checkprettier:
checkblack:
poetry run black --check .
checkisort:
poetry run isort --check-only .
checkeditorconfig:
editorconfig-checker
dev:
poetry run lnbits --reload
test:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
@@ -51,42 +51,66 @@ test-real-wallet:
poetry run pytest
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
unzip tests/data/mock_data.zip -d ./migration-data
LNBITS_ADMIN_UI=True \
make test
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATA_FOLDER="./migration-data" \
LNBITS_DATA_FOLDER="./tests/data" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
HOST=0.0.0.0 \
PORT=5002 \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
timeout 5s poetry run lnbits --host 0.0.0.0 --port 5002 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
LNBITS_DATA_FOLDER="./migration-data" \
LNBITS_DATA_FOLDER="./tests/data" \
LNBITS_DATABASE_URL="postgres://lnbits:lnbits@localhost:5432/migration" \
poetry run python tools/conv.py
migration:
poetry run python tools/conv.py
openapi:
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
HOST=0.0.0.0 \
PORT=5003 \
poetry run lnbits &
sleep 15
curl -s http://0.0.0.0:5003/openapi.json | poetry run openapi-spec-validator --errors=all -
# kill -9 %1
bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
#
updatevendor:
sass:
npm run sass
bundle:
npm install
cp ./node_modules/moment/moment.js ./lnbits/static/vendor/
cp ./node_modules/underscore/underscore.js ./lnbits/static/vendor/
cp ./node_modules/axios/dist/axios.js ./lnbits/static/vendor/
cp ./node_modules/vue/dist/vue.js ./lnbits/static/vendor/
cp ./node_modules/vue-router/dist/vue-router.js ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.browser.js ./lnbits/static/vendor/
cp ./node_modules/@chenfengyuan/vue-qrcode/dist/vue-qrcode.js ./lnbits/static/vendor/
cp ./node_modules/vuex/dist/vuex.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.ie.polyfills.umd.min.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.umd.js ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.bundle.js ./lnbits/static/vendor/
cp ./node_modules/quasar/dist/quasar.css ./lnbits/static/vendor/
cp ./node_modules/chart.js/dist/Chart.css ./lnbits/static/vendor/
cp ./node_modules/vue-qrcode-reader/dist/vue-qrcode-reader.css ./lnbits/static/vendor/
cp ./node_modules/vue-i18n/dist/vue-i18n.js ./lnbits/static/vendor/
npm run sass
npm run vendor_copy
npm run vendor_json
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
npm run vendor_bundle_css
npm run vendor_minify_css
npm run vendor_bundle_js
npm run vendor_minify_js
checkbundle:
cp lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old
cp lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old
make bundle
diff -q lnbits/static/bundle.min.js lnbits/static/bundle.min.js.old || exit 1
diff -q lnbits/static/bundle.min.css lnbits/static/bundle.min.css.old || exit 1
@echo "Bundle is OK"
rm lnbits/static/bundle.min.js.old
rm lnbits/static/bundle.min.css.old
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
+21 -15
View File
@@ -1,13 +1,11 @@
LNbits
======
# LNbits BETA
[![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
# Free and Open-Source Lightning Wallet Accounts System
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
@@ -17,15 +15,24 @@ 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.
LNbits can run on top of any Lightning funding source. It currently supports the following, but more and more are added regularly:
- LND (REST and gRPC)
- Core Lightning aka CLN (gRPC, REST and Spark)
- Eclair
- LNPay
- LNbits
- OpenNode
- Alby
- ZBD
- LightningTipBot
See [LNbits Wiki](https://github.com/lnbits/lnbits/wiki/) for more detailed documentation.
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
@@ -33,7 +40,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits
See the [install guide](https://github.com/lnbits/lnbits/wiki/LNbits-Documentation#install-options) for details on installation and setup.
See the [install guide](https://github.com/lnbits/lnbits/blob/main/docs/guide/installation.md) for details on installation and setup.
## LNbits as an account system
@@ -54,13 +61,13 @@ LNURL has a fallback scheme, so if scanned by a regular QR code reader it can de
![lnurl fallback](https://i.imgur.com/CPBKHIv.png)
Using **lnbits.com/?lightning="LNURL-withdraw"** will trigger a withdraw that builds an LNbits wallet.
Example use would be an ATM, which utilises LNURL, if the user scans the QR with a regular QR code scanner app, they will still be able to access the funds.
Example use would be an ATM, which utilizes LNURL, if the user scans the QR with a regular QR code scanner app, they will still be able to access the funds.
![lnurl ATM](https://i.imgur.com/Gi6bn3L.jpg)
## 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 +75,6 @@ Wallets can be easily generated and given out to people at events. "Go to this
If you like this project [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://github.com/lnbits/lnbits/wiki
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
[github-mypy]: https://github.com/lnbits/lnbits/actions?query=workflow%3Amypy
+1 -1
View File
@@ -1 +1 @@
docs.lnbits.org
docs.lnbits.org
+1 -1
View File
@@ -1,7 +1,7 @@
title: "LNbits docs"
remote_theme: pmarsceill/just-the-docs
color_scheme: dark
logo: "/logos/lnbits-full--inverse.png"
logo: "/logos/lnbits-full-inverse.png"
search_enabled: true
url: https://docs.lnbits.org
aux_links:
+1 -1
View File
@@ -9,4 +9,4 @@ nav_order: 3
API reference
=============
[Swagger Docs](https://docs.lnbits.org/devs/swagger.html)
[Swagger Docs](https://legend.lnbits.com/docs)
+24
View File
@@ -12,6 +12,30 @@ For developers
Thanks for contributing :)
Run
=====
This starts the lnbits uvicorn server
```bash
poetry run lnbits
```
This starts the lnbits uvicorn with hot reloading.
```bash
make dev
# or
poetry run lnbits --reload
```
Precommit hooks
=====
This ensures that all commits adhere to the formatting and linting rules.
```bash
make install-pre-commit-hook
```
Tests
=====
+16 -5
View File
@@ -6,13 +6,13 @@ nav_order: 2
---
Making extensions
Extension set up
=================
Start off by cloning the [example extension](https://github.com/lnbits/example) into your `lnbits/extensions` folder and renaming it to `mysuperplugin`:
Start off by creating a fork of the [example extension](https://github.com/lnbits/example) into own GitHub repository and rename the repository to `mysuperplugin`:
```sh
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 [my-working-folder]
git clone https://github.com/[my-user-name]/mysuperplugin.git --depth=1 # 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'.
@@ -20,7 +20,18 @@ 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.
Going over the example extension's structure:
1. Edit `manifest.json` and change the organisation name to your GitHub username.
1. Push your changes to GitHub.
1. In GitHub create a new release for your extension repo. Tag the release with `0.0.1`
1. Copy the URL of the extension's raw `manifest.json` URL `https://raw.githubusercontent.com/[my-user-name]/mysuperplugin/master/manifest.json`
1. If you are using the LMNbits Admin UI, go to the Admin UI > Server > Extension Sources, click "Add", paste the URL, then click "Save"
1. If you are configuring LNbits via environment variables, add the URL to the .env file's `LNBITS_EXTENSIONS_MANIFESTS` variable. Restart the LNbits python process
1. You will now see your extension in the LNbits > Extensions list. Click "Enable" to enable it.
1. ...
1. Profit!!!
Extension structure explained
-----------------------------
* views_api.py: This is where your public API would go. It will be exposed at "$DOMAIN/$PLUGIN/$ROUTE". For example: https://lnbits.com/mysuperplugin/api/v1/tools.
* views.py: The `/` path will show up as your plugin's home page in lnbits' UI. Other pages you can define yourself. The `templates` folder should explain itself in relation to this.
* migrations.py: Create database tables for your plugin. They'll be created automatically when you start lnbits.
+1 -1
View File
@@ -9,7 +9,7 @@ nav_order: 2
Websockets
=================
`websockets` are a great way to add a two way instant data channel between server and client.
`websockets` are a great way to add a two way instant data channel between server and client.
LNbits has a useful in built websocket tool. With a websocket client connect to (obv change `somespecificid`) `wss://legend.lnbits.com/api/v1/ws/somespecificid` (you can use an online websocket tester). Now make a get to `https://legend.lnbits.com/api/v1/ws/somespecificid/somedata`. You can send data to that websocket by using `from lnbits.core.services import websocketUpdater` and the function `websocketUpdater("somespecificid", "somdata")`.
+15 -11
View File
@@ -8,7 +8,7 @@ nav_order: 4
Admin UI
========
The LNbits Admin UI lets you change LNbits settings via the LNbits frontend.
It is disabled by default and the first time you set the environment variable LNBITS_ADMIN_UI=true
It is disabled by default and the first time you set the environment variable `LNBITS_ADMIN_UI=true`
the settings are initialized and saved to the database and will be used from there as long the UI is enabled.
From there on the settings from the database are used.
@@ -32,15 +32,18 @@ There is also the possibility of posting the super user via webhook to another s
Admin Users
===========
environment variable: LNBITS_ADMIN_USERS, comma-separated list of user ids
Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessable. Also they have access to all the extension defined in LNBITS_ADMIN_EXTENSIONS.
environment variable: `LNBITS_ADMIN_USERS`, comma-separated list of user ids
Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessable. Also they have access to all the extension defined in `LNBITS_ADMIN_EXTENSIONS`.
Allowed Users
=============
environment variable: LNBITS_ALLOWED_USERS, comma-separated list of user ids
environment variable: `LNBITS_ALLOWED_USERS`, comma-separated list of user ids
By defining this users, LNbits will no longer be usable by the public, only defined users and admins can then access the LNbits frontend.
Setting this environment variable also disables account creation.
Account creation can be also disabled by setting `LNBITS_ALLOW_NEW_ACCOUNTS=false`
How to activate
=============
@@ -52,14 +55,15 @@ $ sudo nano .env
-> set: `LNBITS_ADMIN_UI=true`
Now start LNbits once in the terminal window
```
$ poetry run lnbits
```
It will now show you the Super User Account:
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain.
$ poetry run lnbits
```
You can now `cat` the Super User ID:
```
$ cat data/.super_user
123de4bfdddddbbeb48c8bc8382fe123
```
You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain.
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
+5 -5
View File
@@ -15,7 +15,7 @@ An `Extension Manifest` is a link to a `JSON` file which contains information ab
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).
**LNbits** administrators should configure their instances to use repositories that they trust (like the [lnbits-extensions](https://github.com/lnbits/lnbits-extensions/) one).
> **Warning**
> Extensions can have bugs or malicious code, be careful what you install!!
@@ -40,9 +40,9 @@ Click the `Manage` button in order to install a particular release of the extens
Select the version to be installed (usually the last one) and click `Install`. One can also check the `Release Notes` first.
> **Note**:
>
>
> For Github repository: the order of the releases is the one in the GitHub releases page
>
>
> For Explicit Release: the order of the releases is the one in the "extensions" object
@@ -109,12 +109,12 @@ It goes under the `extensions` object and it is of the form:
| htmlUrl | string | optional | Link to the extension home page. |
| infoNotification | string | optional | Users that have this release installed will see a info message for their extension. For example if the extension support will be terminated soon. |
| criticalNotification | string | optional | Reserved for urgent notifications. The admin user will receive a message each time it visits the `Install` page. One example is if the extension has a critical bug. |
| dependencies | list | optional | A list of extension IDs. It signals that those extensions must be installed BEFORE the this one can be installed.
| dependencies | list | optional | A list of extension IDs. It signals that those extensions must be installed BEFORE the this one can be installed.
</details>
This mode has the advantage of strictly specifying what releases of an extension can be installed.
### GitHub Repository
It goes under the `repos` object and it is of the form:
+30 -30
View File
@@ -19,24 +19,24 @@ allow-self-payment=1
```
</details>
<details><summary>Funding source only available via tor (e.g. Umbrel)</summary>
<details><summary>Funding source only available via tor (e.g. Start9 or Umbrel)</summary>
<p>If you want your setup to stay behind tor then only apps, pos and wallets that have tor activated can communicate with your wallets. Most likely you will have trouble when people try to redeem your voucher through onion or when importing your lnbits wallets into a wallet-app that doesnt support tor. If you plan to let LNbits wallets interact with plain internet shops and services you should consider <a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">setting up hybrid mode for your node</a>.</p>
</details>
<details><summary>Funding source is in a cloud</summary>
<p>This means that you might not have access to some files which would allow certain administrative functions. E.g. on <a href="https://voltage.cloud/">Voltage</a> lnd.conf can not be edited. Payments from your node to LNbits wallets can therefore not be configurated in this case atm so you will need to take an extra wallet to send from funding source->wallet x->LNbits wallet (only) for the initial funding of the wallet.</p>
</details>
<details><summary>LNbits via clearnet domain</summary>
<p><a href="https://github.com/TrezorHannes/Dual-LND-Hybrid-VPS">Step by step guide how to convert your Tor only node</a> into a clearnet node to make apps like LNbits accessible via https.</p>
</details>
<details><summary>Which funding sources can I use for LNbits?</summary>
<p>There are several ways to run a LNbits instance funded from different sources. It is important to choose a source that has a good liquidity and good peers connected. If you use LNbits for public services your users´ payments can then flow happily in both directions. If you would like to fund your LNbits wallet via btc please see section Troubleshooting.</p>
<p>The <a href="http://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each: CLN, LND, LNPay, Cliche, OpenNode as well as bots.</p>
<p>The <a href="https://docs.lnbits.org/guide/wallets.html">LNbits manual</a> shows you which sources can be used and how to configure each.</p>
</details>
<!--Later to be added
<details><summary>Advanced setup options</summary>
<p>more text coming soon...</p>
@@ -47,7 +47,7 @@ allow-self-payment=1
<p>When you run your LNbits in clearnet basically everyone can generate a wallet on it. Since the funds of your node are bound to these wallets you might want to prevent that. There are two ways to do so:</p>
<ul>
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/lnbits/blob/main/.env.example">in the .env file</a></li>
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/usermanager">via the Usermanager-Extension</a>. You can find <a href="http://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
<li>Configure allowed users & extensions <a href="https://github.com/lnbits/usermanager">via the Usermanager-Extension</a>. You can find <a href="https://docs.lnbits.org/guide/admin_ui.html">more info about the superuser and the admin extension here</a></li>
</ul>
<p>Please note that all entries in the .env file will not be the taken into account once you activated the admin extension.</p>
</details>
@@ -61,7 +61,7 @@ allow-self-payment=1
<details><summary>LNbits is running via Tor only, you can't open it on a public domain like lnbits.yourdomain.com</summary>
<ul>
<li>Given that you want your setup to stay like this open your LNbits wallet using the .onion URI and create it again. In this way the QR is generated to be accessible via this .onion URI so via tor only. Do not generate that QR from a .local URI, because it will not be reachable via internet - only from within your home-LAN.</li>
<li>Open your LN wallet app that you used to scan that QR and this time by using tor (see wallet app settings).
<li>Open your LN wallet app that you used to scan that QR and this time by using tor (see wallet app settings).
If the app doesn't offer tor, you can use Orbot (Android) instead. See section Installation->Clearnet for detailed instructions.</li>
</ul>
</details>
@@ -70,10 +70,10 @@ allow-self-payment=1
<details><summary>LNbits is running via Tor only, you want to offer public LN services via https</summary>
<ul>
<li>For this we need to partially open LNbits to a clearnet (domain/IP) through a https SSL certificate. Follow the instructions from <a href="https://docs.lnbits.org/guide/installation.html#reverse-proxy-with-automatic-https-using-caddy">this LNbits caddy installation instruction</a>.
You need to have a domain and to be able to configure a CNAME for your DNS record as well as generate a subdomain dedicated to your LNbits instance like eg. lnbits.yourdomain.com.
You need to have a domain and to be able to configure a CNAME for your DNS record as well as generate a subdomain dedicated to your LNbits instance like eg. lnbits.yourdomain.com.
You also need access to your internet router to open the https port (usually 443) and forward it your LNbits IP within your LAN (usually 80). The ports might depend on your node implementation if those ports do not work please ask for them in a help group of your node supplier.</li>
<li>You can also follow the Apache installation option, explained in the <a href="https://docs.lnbits.org/guide/installation.html#running-behind-an-apache2-reverse-proxy-over-https">LNbits installation manual</a>.</li>
<li>If you run LNbits from a bundle node (Umbrel, myNode, Embassy, Raspiblitz etc), you can follow <a href="https://github.com/TrezorHannes/vps-lnbits">this extensive guide</a> with many options to switch your Tor only LNbits into a clearnet LNbits. For Citadel there is a HTTPS Option in your manual to activate https for LNbits in the newest version.</li>
<li>If you run LNbits from a bundle node (myNode, Start9, Umbrel,Raspiblitz etc), you can follow <a href="https://github.com/TrezorHannes/vps-lnbits">this extensive guide</a> with many options to switch your Tor only LNbits into a clearnet LNbits. For Citadel there is a HTTPS Option in your manual to activate https for LNbits in the newest version.</li>
</ul>
</details>
</li>
@@ -89,9 +89,9 @@ allow-self-payment=1
</li>
<li>
<details><summary>Wallet on your own funding source/node</summary>
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place.
You can find all LNbits users and wallet-IDs in your LNbits user manager extension or in your sqlite database.
To edit or read the LNbits database, go to the LNbits /data folder and look for the file called sqlite.db.
<p>Always save a copy of your wallet-URL, Export2phone-QR or LNDhub for your own wallets in a safe place.
You can find all LNbits users and wallet-IDs in your LNbits user manager extension or in your sqlite database.
To edit or read the LNbits database, go to the LNbits /data folder and look for the file called sqlite.db.
You can open and edit it with excel or with a dedicated SQL-Editor like <a href="https://sqlitebrowser.org/">SQLite browser</a>.</p>
</details>
</li>
@@ -100,7 +100,7 @@ allow-self-payment=1
<details><summary>Configure a comment that people see when paying to my LNURLp QR</summary>
<p>When you create a LNURL-p, by default the comment box is not filled. That means comments are not allowed to be attached to payments.<p>
<p>In order to allow comments, add the characters lenght of the box, from 1 to 250. Once you put a number there,
<p>In order to allow comments, add the characters lenght of the box, from 1 to 250. Once you put a number there,
the comment box will be displayed in the payment process. You can also edit a LNURL-p already created and add that number.</p>
![lnbits-lnurl-comment.png](https://i.postimg.cc/HkJQ9xKr/lnbits-lnurl-comment.png)
@@ -113,16 +113,16 @@ allow-self-payment=1
<li>
<details><summary>A - Via an external swap service</summary>
<p>If the user do not have full access of your LNbits, is just an external user, can use swap services like <a href="https://boltz.exchange/">Boltz</a>, <a href="https://fixedfloat.com/">FixedFloat</a>, <a href="https://swap.diamondhands.technology/">DiamondHands</a> or <a href="https://zigzag.io/">ZigZag</a>.</p>
<p>This is useful if you provide only LNURL/LN invoices from your LNbits instance, but a payer only has onchain sats so
<p>This is useful if you provide only LNURL/LN invoices from your LNbits instance, but a payer only has onchain sats so
they will have to the swap those sats first on their side.</p>
<p>The procedure is simple: user sends onchain btc to the swap service and provides the LNURL / LN invoice from LNbits as destination of the swap.</p>
</details>
</li>
<li>
<details><summary>B - Using the Onchain LNbits extension</summary>
<p>Keep in mind that this is a separate wallet, not the LN btc one that is represented by LNbits as "your wallet" upon your LN funding source.
This onchain wallet can be used also to swap LN btc to (e.g. your hardwarewallet) by using the LNbits Boltz or Deezy extension.
If you run a webshop that is linked to your LNbits for LN payments, it is very handy to regularly drain all the sats from LN into onchain.
<p>Keep in mind that this is a separate wallet, not the LN btc one that is represented by LNbits as "your wallet" upon your LN funding source.
This onchain wallet can be used also to swap LN btc to (e.g. your hardwarewallet) by using the LNbits Boltz or Deezy extension.
If you run a webshop that is linked to your LNbits for LN payments, it is very handy to regularly drain all the sats from LN into onchain.
This leads to more space in your LN channels to be able to receive new fresh sats.</p>
<p>Procedure:</p>
<ul>
@@ -130,25 +130,25 @@ allow-self-payment=1
<li>Go to wallet information and copy the xpub</li>
<li>Go to LNbits - Onchain extension and create a new watch-only wallet with that xpub</li>
<li>Go to LNbits - Tipjar extension and create a new Tipjar. Select also the onchain option besides the LN wallet.</li>
<li>Optional - Go to LNbits - SatsPay extension and create a new charge for onchain btc.
<li>Optional - Go to LNbits - SatsPay extension and create a new charge for onchain btc.
You can choose between onchain and LN or both. It will then create an invoice that can be shared.</li>
<li>Optional - If you use your LNbits linked to a Wordpress + Woocommerce page, once you create/link a watch-only wallet to your LN btc shop wallet,
<li>Optional - If you use your LNbits linked to a Wordpress + Woocommerce page, once you create/link a watch-only wallet to your LN btc shop wallet,
the customer will have both options to pay on the same screen.</li>
</ul>
</details>
</li>
</ul>
</details>
<details><summary>Where can I see payment details?</summary>
<p>When you receive a payment in LNbits, the transaction log will display only a resumed type of the transaction.
<p>When you receive a payment in LNbits, the transaction log will display only a resumed type of the transaction.
![lnbits-tx-log.png](https://i.postimg.cc/gk2FMFG9/lnbits-tx-log.png)
<p>In your transaction overview you will find a little green arrow for received and a red arrow for sended funds.<p>
<p>If you click on those arrows, a details popup shows attached messages as well as the sender´s name if given.</p>
</details>
<details><summary>Can I configure a name to the payments i make?</summary>
<p>In LNbits this is currently not possible to do - but to receive. This is only possible if the sender's LN wallet supports <a href="https://github.com/lnurl/luds">LUD-18</a> (nameDesc) like e.g. <a href="https://darthcoin.substack.com/p/obw-open-bitcoin-wallet">Open Bitcion Wallet - OBW</a> does. You will then see an alias/pseudonym in the details section of your LNbits transactions (click the arrows). Note that you can give any name there and it might not be related to the real sender´s name(!) if your receive such.</p>
![lnbits-tx-details.png](https://i.postimg.cc/yYnvyK4w/lnbits-tx-details.png)
@@ -171,7 +171,7 @@ allow-self-payment=1
<details><summary>ATM - deposit and withdraw in your shop or at your meetup</summary>
<p>This is a do-it-yourself project consisting of a mini-computer (Raspberry Pi Zero), a coin acceptor, a display, a 3D printed case, and a Bitcoin Lightning wallet as a funding source. It exchanges fiat coins for valuable Bitcoin Lightning ⚡ Satoshis. The user can pick up the Satoshis via QR code (LNURL-withdraw) on a mobile phone wallet. You can get the components as individual parts and build the case yourself e.g. from <a href="https://www.Fulmo.org">Fulmo</a> who also made a <a href="https://blog.fulmo.org/the-lightningatm-pocket-edition/">guide</a> on it. The shop offers payments in Bitcoin and Lightning ⚡. The code can be found on <a href="https://github.com/21isenough/LightningATM">the ATM github project page></a>.</p>
</details>
<details><summary>POS Terminal - an offline terminal for merchants</summary>
<p>The LNpos is a self-sufficient point of sale terminal which allows offline onchain payments and an offline Lightning ATM for withdrawals. Free and open source software, free from intermediaries, with integrated battery, WLAN, DIY. You can get the 3D print as well as the whole kit from the LNbits shop from 👇 Resources. It allows
<li>LNPoS Online interactive Lightning payments</li>
@@ -183,21 +183,21 @@ allow-self-payment=1
</p>
</p>
</details>
<details><summary>Hardware Wallet- build your own, stack harder</summary>
<p>The hardwarewallet is a very cheap solution for builders. The projects´ <a hrel="https://github.com/lnbits/hardware-wallet">code and installation instructions for the LNbits hardware wallet can be found on github</a></p>
<p>
<img width="546" alt="Bildschirm­foto 2023-01-20 um 18 08 37" src="https://user-images.githubusercontent.com/63317640/213760948-38fd77b0-9247-4505-9433-f5af1b223527.png">
</p>
</details>
<details><summary>Bitcoin Switch - turn things on with bitcoin</summary>
<p>Candy dispenser, vending machines (online), grabbing machines, jukeboxes, bandits and <a href="https://github.com/cryptoteun/awesome-lnbits">all sorts of other things have already been build with LNbits´ tools</a>. Further info see 👇 Resources.</p>
<p>
<img width="549" alt="Bildschirm­foto 2023-01-20 um 18 11 55" src="https://user-images.githubusercontent.com/63317640/213761646-d25d4745-e50d-4389-98e5-f83237a8cf6b.png">
</p>
</details>
<details><summary>Vending machine (offline)</summary>
<p>This code works similar to the LNpos. Note that the <a href=" https://www.youtube.com/watch?v=Fg0UuuzsYXc&t=762s">setup-video for the vending machine</a> misses the new way of installing it via the new LNURLdevices extension. The <a href="https://github.com/arcbtc/LNURLVend">vending machine project code resides on github</a>.</p>
<p>
@@ -262,7 +262,7 @@ allow-self-payment=1
</ul>
## Developing for LNbits
<ul>
<li><a href="http://docs.lnbits.org/devs/development.html">Making Estension / how to use Websockets / API reference</a></li>
<ul>
<li><a href="https://docs.lnbits.org/devs/development.html">Making extensions / How to use Websockets / API reference</a></li>
<li><a href="https://t.me/lnbits">Telegram LNbits Support Group</a></li></ul>
</ul>
+6 -6
View File
@@ -2,7 +2,7 @@
## Defining a route with path parameters
**old:**
```python
# with <>
# with <>
@offlineshop_ext.route("/lnurl/<item_id>", methods=["GET"])
```
@@ -35,8 +35,8 @@ async def extensions(user: User = Depends(check_user_exists)):
```python
return (
{
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
},
HTTPStatus.OK,
@@ -47,8 +47,8 @@ FastAPI returns `HTTPStatus.OK` by default id no Exception is raised
**new:**
```python
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
}
```
@@ -119,4 +119,4 @@ Instead of hitting the database over and over again, we can store a short lived
Example:
* Get transactions for a wallet ID
* User data for a user id
* Wallet data for a Admin / Invoice key
* Wallet data for a Admin / Invoice key
+29 -17
View File
@@ -12,11 +12,12 @@ By default, LNbits will use SQLite as its database. You can also use PostgreSQL
## Option 1 (recommended): poetry
If you have problems installing LNbits using these instructions, please have a look at the [Troubleshooting](#troubleshooting) section.
Mininum poetry version has is ^1.2, but it is recommended to use latest poetry. (including OSX)
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
git checkout main
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
sudo apt update
@@ -60,44 +61,59 @@ poetry install --only main
## Option 2: Nix
> note: currently not supported while we make some architectural changes on the path to leave beta
```sh
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
sh <(curl -L https://nixos.org/nix/install) --daemon
# Enable nix-command and flakes experimental features for nix:
echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf
# Add cachix for cached binaries
nix-env -iA cachix -f https://cachix.org/api/v1/install
cachix use lnbits
# Clone and build LNbits
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# Modern debian distros usually include Nix, however you can install with:
# 'sh <(curl -L https://nixos.org/nix/install) --daemon', or use setup here https://nixos.org/download.html#nix-verify-installation
nix build
nix build .#lnbits
mkdir data
```
#### Running the server
```sh
# .env variables are currently passed when running
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
nix run
```
Ideally you would set the environment via the `.env` file,
but you can also set the env variables or pass command line arguments:
``` sh
# .env variables are currently passed when running, but LNbits can be managed with the admin UI.
LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
# Once you have created a user, you can set as the super_user
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
```
## Option 3: Docker
use latest version from docker hub
```sh
docker pull lnbitsdocker/lnbits-legend
docker pull lnbits/lnbits
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
build the image yourself
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbitsdocker/lnbits-legend .
docker build -t lnbits/lnbits .
cp .env.example .env
mkdir data
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
## Option 4: Fly.io
@@ -190,10 +206,6 @@ sudo apt install python3.9-dev gcc build-essential
poetry add setuptools wheel
```
#### Poetry
If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
+26 -5
View File
@@ -8,7 +8,7 @@ nav_order: 3
Backend wallets
===============
LNbits can run on top of many lightning-network funding sources. Currently there is support for CoreLightning, LND, LNbits, LNPay and OpenNode, with more being added regularly.
LNbits can run on top of many lightning-network funding sources with more being added regularly.
A backend wallet can be configured using the following LNbits environment variables:
@@ -18,7 +18,14 @@ 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)
### CoreLightning REST
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
- `CORELIGHTNING_REST_CERT`: /home/lightning/clnrest/tls.cert
### Spark (Core Lightning)
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
- `SPARK_URL`: http://10.147.17.230:9737/rpc
@@ -29,7 +36,7 @@ A backend wallet can be configured using the following LNbits environment variab
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
- `LND_REST_CERT`: /file/path/tls.cert
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
or
@@ -41,7 +48,7 @@ or
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Base64/Hex
You can also use an AES-encrypted macaroon (more info) instead by using
@@ -64,7 +71,6 @@ For the invoice listener to work you have a publicly accessible URL in your LNbi
- `LNPAY_API_KEY`: sak_apiKey
- `LNPAY_WALLET_KEY`: waka_apiKey
### OpenNode
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
@@ -73,6 +79,21 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
### Alby
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
- `LNBITS_BACKEND_WALLET_CLASS`: **AlbyWallet**
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
### ZBD
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an ZBD API Key here: https://zbd.dev/docs/dashboard/projects/api
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
- `ZBD_API_KEY`: ZBDApiKey
### Cliche Wallet
+1 -1
View File
@@ -11,7 +11,7 @@ LNbits, free and open-source lightning-network wallet/accounts system
LNbits is a very simple Python application that sits on top of any funding source, and 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 LNbits extension framework
* Extendable platform for exploring lightning-network functionality via LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.041 30.0001"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g fill="#eee" font-family="roboto"><path d="M33.6755 148.518h.0962v.0364h-.1416v-.22h.0454zM33.9796 148.5544h-.0453l-.0883-.1448v.1448h-.0453v-.22h.0453l.0884.145v-.145h.0452z" font-weight="700" style="-inkscape-font-specification:'roboto Bold'"/><path d="M34.1625 148.4744q0 .0375-.0172.0603-.0172.0227-.0462.0227-.031 0-.048-.0219l-.0014.019h-.0256v-.2322h.028v.0866q.0168-.021.0466-.021.0298 0 .0467.0225.017.0225.017.0617zm-.028-.003q0-.0286-.011-.044-.011-.0157-.0317-.0157-.0277 0-.0398.0257v.0707q.0129.0257.04.0257.0202 0 .0314-.0156.0112-.0156.0112-.0469zM34.231 148.5544h-.028v-.1635h.028zm-.0302-.2069q0-.007.0041-.0115.0042-.005.0124-.005.0082 0 .0124.005.0042.005.0042.0115 0 .007-.0042.0113-.0042.005-.0124.005-.0082 0-.0124-.005-.004-.005-.004-.0113zM34.3167 148.3513v.0396h.0305v.0216h-.0305v.1014q0 .01.004.0148.0042.005.014.005.0048 0 .0133-.002v.0227q-.011.003-.0215.003-.0187 0-.0283-.0113-.0095-.0113-.0095-.0322v-.1014h-.0297v-.0216h.0297v-.0396zM34.478 148.511q0-.0113-.0086-.0175-.0085-.006-.0298-.0109-.0211-.005-.0337-.0109-.0124-.006-.0184-.015t-.006-.0209q0-.02.017-.034.017-.0139.0435-.0139.0278 0 .045.0144.0174.0144.0174.0367h-.028q0-.0115-.0099-.0198-.0097-.008-.0245-.008-.0152 0-.0238.007-.0086.007-.0086.0174 0 .0101.008.0153.008.005.0288.01.021.005.034.0112.013.007.0192.0157.0063.009.0063.0222 0 .022-.0175.0352-.0175.0131-.0455.0131-.0197 0-.0348-.007-.015-.007-.0237-.0193-.0085-.0125-.0085-.027h.028q.0007.014.0111.0223.0106.008.0279.008.0158 0 .0253-.006.0097-.006.0097-.0172z" style="-inkscape-font-specification:roboto"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="3032" height="902" viewBox="0 0 3032 902" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M158.566 493.857L1 901L450.49 355.202H264.831L501.791 1H187.881L36.4218 493.857H158.566Z" fill="#FF1FE1" stroke="black"/>
<path d="M616.041 764.863V134.611H741.647V645.115H1023.41V764.863H616.041Z" fill="#F8F8F8"/>
<path d="M1079.99 764.863V134.611H1198.81L1478.31 543.014V134.611H1600.52V764.863H1478.31L1198.81 360.241V764.863H1079.99Z" fill="#F8F8F8"/>
<path d="M616.041 764.863V134.611H741.647V645.115H1023.41V764.863H616.041Z" stroke="black"/>
<path d="M1079.99 764.863V134.611H1198.81L1478.31 543.014V134.611H1600.52V764.863H1478.31L1198.81 360.241V764.863H1079.99Z" stroke="black"/>
<path d="M1693.87 768.522V148.458H1764.1V377.349H1770.05C1775.21 369.275 1782.35 358.981 1791.47 346.467C1800.8 333.751 1814.09 322.448 1831.35 312.557C1848.81 302.465 1872.41 297.419 1902.17 297.419C1940.66 297.419 1974.58 307.208 2003.94 326.787C2033.3 346.366 2056.22 374.119 2072.68 410.048C2089.15 445.976 2097.38 488.363 2097.38 537.209C2097.38 586.459 2089.15 629.149 2072.68 665.279C2056.22 701.207 2033.4 729.061 2004.24 748.842C1975.08 768.421 1941.45 778.21 1903.36 778.21C1874 778.21 1850.49 773.265 1832.84 763.375C1815.18 753.283 1801.59 741.878 1792.07 729.162C1782.55 716.244 1775.21 705.547 1770.05 697.069H1761.72V768.522H1693.87ZM1762.91 535.998C1762.91 571.119 1767.96 602.102 1778.08 628.947C1788.2 655.59 1802.98 676.481 1822.42 691.619C1841.86 706.556 1865.67 714.024 1893.84 714.024C1923.2 714.024 1947.7 706.152 1967.34 690.408C1987.18 674.463 2002.06 653.067 2011.98 626.222C2022.1 599.175 2027.15 569.1 2027.15 535.998C2027.15 503.299 2022.19 473.83 2012.28 447.59C2002.55 421.149 1987.78 400.258 1967.94 384.918C1948.3 369.376 1923.6 361.605 1893.84 361.605C1865.27 361.605 1841.27 368.972 1821.83 383.707C1802.38 398.24 1787.7 418.626 1777.78 444.866C1767.87 470.903 1762.91 501.281 1762.91 535.998Z" fill="white"/>
<path d="M2204.81 768.522V303.474H2275.04V768.522H2204.81ZM2240.52 225.966C2226.83 225.966 2215.03 221.223 2205.11 211.736C2195.39 202.25 2190.52 190.846 2190.52 177.524C2190.52 164.202 2195.39 152.798 2205.11 143.311C2215.03 133.825 2226.83 129.081 2240.52 129.081C2254.21 129.081 2265.91 133.825 2275.63 143.311C2285.55 152.798 2290.51 164.202 2290.51 177.524C2290.51 190.846 2285.55 202.25 2275.63 211.736C2265.91 221.223 2254.21 225.966 2240.52 225.966Z" fill="white"/>
<path d="M2602.45 303.474V364.027H2365.57V303.474H2602.45ZM2434.61 192.057H2504.84V635.305C2504.84 655.489 2507.72 670.628 2513.47 680.72C2519.42 690.61 2526.96 697.271 2536.09 700.702C2545.41 703.932 2555.23 705.547 2565.55 705.547C2573.28 705.547 2579.63 705.143 2584.59 704.335C2589.55 703.326 2593.52 702.519 2596.49 701.913L2610.78 766.1C2606.02 767.916 2599.37 769.733 2590.84 771.549C2582.31 773.568 2571.5 774.577 2558.41 774.577C2538.57 774.577 2519.12 770.237 2500.08 761.558C2481.23 752.879 2465.56 739.658 2453.06 721.896C2440.76 704.134 2434.61 681.729 2434.61 654.682V192.057Z" fill="white"/>
<path d="M3023.67 407.625L2960.58 425.791C2956.61 415.094 2950.76 404.699 2943.02 394.607C2935.49 384.313 2925.17 375.835 2912.08 369.174C2898.98 362.513 2882.22 359.183 2861.79 359.183C2833.81 359.183 2810.5 365.743 2791.85 378.863C2773.4 391.781 2764.18 408.231 2764.18 428.214C2764.18 445.976 2770.53 460.004 2783.22 470.298C2795.92 480.592 2815.76 489.17 2842.74 496.033L2910.59 512.988C2951.46 523.08 2981.91 538.521 3001.94 559.311C3021.98 579.899 3032 606.441 3032 638.938C3032 665.581 3024.46 689.399 3009.38 710.391C2994.51 731.383 2973.67 747.934 2946.89 760.044C2920.11 772.155 2888.96 778.21 2853.45 778.21C2806.83 778.21 2768.25 767.916 2737.7 747.328C2707.14 726.74 2687.8 696.665 2679.67 657.104L2746.33 640.149C2752.67 665.178 2764.68 683.949 2782.33 696.464C2800.19 708.978 2823.5 715.235 2852.26 715.235C2885 715.235 2910.99 708.17 2930.23 694.041C2949.67 679.711 2959.39 662.554 2959.39 642.571C2959.39 626.424 2953.84 612.9 2942.73 602.001C2931.62 590.899 2914.56 582.624 2891.54 577.174L2815.36 559.008C2773.5 548.916 2742.75 533.273 2723.11 512.08C2703.67 490.684 2693.95 463.94 2693.95 431.847C2693.95 405.607 2701.19 382.395 2715.67 362.211C2730.36 342.026 2750.29 326.182 2775.49 314.676C2800.88 303.171 2829.65 297.419 2861.79 297.419C2907.02 297.419 2942.53 307.511 2968.32 327.695C2994.31 347.88 3012.76 374.523 3023.67 407.625Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+11 -1
View File
@@ -1 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.041 30.0001"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g font-family="roboto"><path d="M33.6755 148.518h.0962v.0364h-.1416v-.22h.0454zM33.9796 148.5544h-.0453l-.0883-.1448v.1448h-.0453v-.22h.0453l.0884.145v-.145h.0452z" font-weight="700" style="-inkscape-font-specification:'roboto Bold'"/><path d="M34.1625 148.4744q0 .0375-.0172.0603-.0172.0227-.0462.0227-.031 0-.048-.0219l-.0014.019h-.0256v-.2322h.028v.0866q.0168-.021.0466-.021.0298 0 .0467.0225.017.0225.017.0617zm-.028-.003q0-.0286-.011-.044-.011-.0157-.0317-.0157-.0277 0-.0398.0257v.0707q.0129.0257.04.0257.0202 0 .0314-.0156.0112-.0156.0112-.0469zM34.231 148.5544h-.028v-.1635h.028zm-.0302-.2069q0-.007.0041-.0115.0042-.005.0124-.005.0082 0 .0124.005.0042.005.0042.0115 0 .007-.0042.0113-.0042.005-.0124.005-.0082 0-.0124-.005-.004-.005-.004-.0113zM34.3167 148.3513v.0396h.0305v.0216h-.0305v.1014q0 .01.004.0148.0042.005.014.005.0048 0 .0133-.002v.0227q-.011.003-.0215.003-.0187 0-.0283-.0113-.0095-.0113-.0095-.0322v-.1014h-.0297v-.0216h.0297v-.0396zM34.478 148.511q0-.0113-.0086-.0175-.0085-.006-.0298-.0109-.0211-.005-.0337-.0109-.0124-.006-.0184-.015t-.006-.0209q0-.02.017-.034.017-.0139.0435-.0139.0278 0 .045.0144.0174.0144.0174.0367h-.028q0-.0115-.0099-.0198-.0097-.008-.0245-.008-.0152 0-.0238.007-.0086.007-.0086.0174 0 .0101.008.0153.008.005.0288.01.021.005.034.0112.013.007.0192.0157.0063.009.0063.0222 0 .022-.0175.0352-.0175.0131-.0455.0131-.0197 0-.0348-.007-.015-.007-.0237-.0193-.0085-.0125-.0085-.027h.028q.0007.014.0111.0223.0106.008.0279.008.0158 0 .0253-.006.0097-.006.0097-.0172z" style="-inkscape-font-specification:roboto"/></g></g></svg>
<svg width="3032" height="902" viewBox="0 0 3032 902" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M158.566 493.857L1 901L450.49 355.202H264.831L501.791 1H187.881L36.4218 493.857H158.566Z" fill="#FF1FE1" stroke="black"/>
<path d="M616.041 764.863V134.611H741.647V645.115H1023.41V764.863H616.041Z" fill="#1E1E1E"/>
<path d="M1079.99 764.863V134.611H1198.81L1478.31 543.014V134.611H1600.52V764.863H1478.31L1198.81 360.241V764.863H1079.99Z" fill="#1E1E1E"/>
<path d="M616.041 764.863V134.611H741.647V645.115H1023.41V764.863H616.041Z" stroke="black"/>
<path d="M1079.99 764.863V134.611H1198.81L1478.31 543.014V134.611H1600.52V764.863H1478.31L1198.81 360.241V764.863H1079.99Z" stroke="black"/>
<path d="M1693.87 768.522V148.458H1764.1V377.349H1770.05C1775.21 369.275 1782.35 358.981 1791.47 346.467C1800.8 333.751 1814.09 322.448 1831.35 312.557C1848.81 302.465 1872.41 297.419 1902.17 297.419C1940.66 297.419 1974.58 307.208 2003.94 326.787C2033.3 346.366 2056.22 374.119 2072.68 410.048C2089.15 445.976 2097.38 488.363 2097.38 537.209C2097.38 586.459 2089.15 629.149 2072.68 665.279C2056.22 701.207 2033.4 729.061 2004.24 748.842C1975.08 768.421 1941.45 778.21 1903.36 778.21C1874 778.21 1850.49 773.265 1832.84 763.375C1815.18 753.282 1801.59 741.878 1792.07 729.162C1782.55 716.244 1775.21 705.547 1770.05 697.069H1761.72V768.522H1693.87ZM1762.91 535.998C1762.91 571.119 1767.96 602.102 1778.08 628.947C1788.2 655.59 1802.98 676.481 1822.42 691.619C1841.86 706.556 1865.67 714.024 1893.84 714.024C1923.2 714.024 1947.7 706.152 1967.34 690.408C1987.18 674.463 2002.06 653.067 2011.98 626.222C2022.1 599.175 2027.15 569.1 2027.15 535.998C2027.15 503.299 2022.2 473.83 2012.28 447.59C2002.55 421.149 1987.78 400.258 1967.94 384.918C1948.3 369.376 1923.6 361.605 1893.84 361.605C1865.27 361.605 1841.27 368.972 1821.83 383.707C1802.38 398.24 1787.7 418.626 1777.78 444.866C1767.87 470.903 1762.91 501.281 1762.91 535.998Z" fill="black"/>
<path d="M2204.81 768.522V303.474H2275.04V768.522H2204.81ZM2240.52 225.966C2226.83 225.966 2215.03 221.223 2205.11 211.736C2195.39 202.25 2190.52 190.846 2190.52 177.524C2190.52 164.202 2195.39 152.798 2205.11 143.311C2215.03 133.825 2226.83 129.081 2240.52 129.081C2254.21 129.081 2265.91 133.825 2275.63 143.311C2285.55 152.798 2290.51 164.202 2290.51 177.524C2290.51 190.846 2285.55 202.25 2275.63 211.736C2265.91 221.223 2254.21 225.966 2240.52 225.966Z" fill="black"/>
<path d="M2602.45 303.474V364.027H2365.57V303.474H2602.45ZM2434.61 192.057H2504.84V635.305C2504.84 655.489 2507.72 670.628 2513.47 680.72C2519.42 690.61 2526.96 697.271 2536.09 700.702C2545.41 703.932 2555.23 705.546 2565.55 705.546C2573.28 705.546 2579.63 705.143 2584.59 704.335C2589.55 703.326 2593.52 702.519 2596.5 701.913L2610.78 766.1C2606.02 767.916 2599.37 769.733 2590.84 771.549C2582.31 773.568 2571.5 774.577 2558.41 774.577C2538.57 774.577 2519.12 770.237 2500.08 761.558C2481.23 752.879 2465.56 739.658 2453.06 721.896C2440.76 704.134 2434.61 681.729 2434.61 654.682V192.057Z" fill="black"/>
<path d="M3023.67 407.625L2960.58 425.791C2956.61 415.094 2950.76 404.699 2943.02 394.607C2935.49 384.313 2925.17 375.835 2912.08 369.174C2898.98 362.513 2882.22 359.183 2861.79 359.183C2833.81 359.183 2810.5 365.743 2791.85 378.863C2773.41 391.781 2764.18 408.231 2764.18 428.213C2764.18 445.976 2770.53 460.004 2783.23 470.298C2795.92 480.592 2815.76 489.17 2842.74 496.033L2910.59 512.988C2951.46 523.08 2981.91 538.521 3001.94 559.311C3021.98 579.899 3032 606.441 3032 638.938C3032 665.581 3024.46 689.399 3009.38 710.391C2994.51 731.383 2973.67 747.934 2946.89 760.044C2920.11 772.155 2888.96 778.21 2853.45 778.21C2806.83 778.21 2768.25 767.916 2737.7 747.328C2707.14 726.74 2687.8 696.665 2679.67 657.104L2746.33 640.149C2752.67 665.178 2764.68 683.949 2782.33 696.464C2800.19 708.978 2823.5 715.235 2852.26 715.235C2885 715.235 2910.99 708.17 2930.23 694.041C2949.67 679.711 2959.39 662.554 2959.39 642.571C2959.39 626.424 2953.84 612.9 2942.73 602.001C2931.62 590.899 2914.56 582.624 2891.54 577.174L2815.36 559.008C2773.5 548.916 2742.75 533.273 2723.11 512.079C2703.67 490.684 2693.95 463.94 2693.95 431.847C2693.95 405.607 2701.19 382.395 2715.67 362.211C2730.36 342.026 2750.29 326.182 2775.49 314.676C2800.88 303.171 2829.65 297.419 2861.79 297.419C2907.02 297.419 2942.53 307.511 2968.32 327.695C2994.31 347.88 3012.76 374.523 3023.67 407.625Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+4 -1
View File
@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><g transform="translate(-2.9464 -5.625) scale(.17717)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g></svg>
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="450" cy="450" r="450" fill="#1E1E1E"/>
<path d="M387.802 476.423L281.613 730.887L584.54 389.763H459.418L619.113 168.387H407.558L305.485 476.423H387.802Z" fill="#FF1FE1"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 290 B

Generated
+88 -11
View File
@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
@@ -15,18 +18,39 @@
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1698974481,
"narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "4bb5e752616262457bc7ca5882192a564c0472d2",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1678470307,
"narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=",
"lastModified": 1702233072,
"narHash": "sha256-H5G2wgbim2Ku6G6w+NSaQaauv6B6DlPhY9fMvArKqRo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f",
"rev": "781e2a9797ecf0f146e81425c822dca69fe4a348",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -34,16 +58,19 @@
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
]
],
"systems": "systems_2",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1678513806,
"narHash": "sha256-bEto1lp9bIQ+DlJsXQyRxN5l6B/oy3Skb7DueYRJJBo=",
"lastModified": 1702334837,
"narHash": "sha256-QZG6+zFshyY+L8m2tlOTm75U5m9y7z01g0josVK+8Os=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "bf239d53fafb53cf439a72b3a50e86dd6a3984a5",
"rev": "1f4bcbf1be73abc232a972a77102a3e820485a99",
"type": "github"
},
"original": {
@@ -57,6 +84,56 @@
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1699786194,
"narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
+10 -11
View File
@@ -2,7 +2,7 @@
description = "LNbits, free and open-source Lightning wallet and accounts system";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
@@ -13,28 +13,27 @@
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forSystems = systems: f:
nixpkgs.lib.genAttrs systems
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlays.default self.overlays.default ]; }));
forAllSystems = forSystems supportedSystems;
projectName = "lnbits";
in
{
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
poetry
];
};
});
overlays = {
default = final: prev: {
${projectName} = self.packages.${final.hostPlatform.system}.${projectName};
${projectName} = self.packages.${prev.stdenv.hostPlatform.system}.${projectName};
};
};
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
meta.rev = self.dirtyRev or self.rev;
meta.mainProgram = projectName;
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
protobuf = prev.protobuf.override { preferWheel = true; };
ruff = prev.ruff.override { preferWheel = true; };
wallycore = prev.wallycore.override { preferWheel = true; };
});
};
});
nixosModules = {
+488
View File
@@ -0,0 +1,488 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
<hashTree>
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
<hashTree>
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Account" enabled="true"/>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get main page" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
<stringProp name="Header.value">1</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Create new account" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{&quot;name&quot;:&quot;a1&quot;}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/account</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract user and wallet" enabled="true">
<stringProp name="cacheKey">true</stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
vars.put(&quot;userId&quot;, resp.user || &quot;no-user-id&quot;);
vars.put(&quot;walletId&quot;, resp.id || &quot;no-wallet-id&quot;);
vars.put(&quot;inkey&quot;, resp.inkey || &apos;no-inkey&apos;);
vars.put(&quot;adminkey&quot;, resp.adminkey || &apos;no-adminkey&apos;);
</stringProp>
<stringProp name="scriptLanguage">javascript</stringProp>
</JSR223PostProcessor>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Login with user id" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{&quot;usr&quot;:&quot;${userId}&quot;}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/auth/usr</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.${paidChargeCount}</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/${paidChargeCount}.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Go to wallet page" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="wal" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.name">wal</stringProp>
<stringProp name="Argument.value">${walletId}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.use_equals">true</boolProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/wallet</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
<stringProp name="Header.value">1</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get currencies" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/currencies</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="X-Api-Key" elementType="Header">
<stringProp name="Header.name">X-Api-Key</stringProp>
<stringProp name="Header.value">undefined</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Currency List" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="1438956759">[&quot;AED&quot;,&quot;AFN&quot;,&quot;ALL&quot;,&quot;AMD&quot;,&quot;ANG&quot;,&quot;AOA&quot;,&quot;ARS&quot;,&quot;AUD&quot;,&quot;AWG&quot;,&quot;AZN&quot;,&quot;BAM&quot;,&quot;BBD&quot;,&quot;BDT&quot;,&quot;BGN&quot;,&quot;BHD&quot;,&quot;BIF&quot;,&quot;BMD&quot;,&quot;BND&quot;,&quot;BOB&quot;,&quot;BRL&quot;,&quot;BSD&quot;,&quot;BTN&quot;,&quot;BWP&quot;,&quot;BYN&quot;,&quot;BYR&quot;,&quot;BZD&quot;,&quot;CAD&quot;,&quot;CDF&quot;,&quot;CHF&quot;,&quot;CLF&quot;,&quot;CLP&quot;,&quot;CNH&quot;,&quot;CNY&quot;,&quot;COP&quot;,&quot;CRC&quot;,&quot;CUC&quot;,&quot;CVE&quot;,&quot;CZK&quot;,&quot;DJF&quot;,&quot;DKK&quot;,&quot;DOP&quot;,&quot;DZD&quot;,&quot;EGP&quot;,&quot;ERN&quot;,&quot;ETB&quot;,&quot;EUR&quot;,&quot;FJD&quot;,&quot;FKP&quot;,&quot;GBP&quot;,&quot;GEL&quot;,&quot;GGP&quot;,&quot;GHS&quot;,&quot;GIP&quot;,&quot;GMD&quot;,&quot;GNF&quot;,&quot;GTQ&quot;,&quot;GYD&quot;,&quot;HKD&quot;,&quot;HNL&quot;,&quot;HRK&quot;,&quot;HTG&quot;,&quot;HUF&quot;,&quot;IDR&quot;,&quot;ILS&quot;,&quot;IMP&quot;,&quot;INR&quot;,&quot;IQD&quot;,&quot;IRT&quot;,&quot;ISK&quot;,&quot;JEP&quot;,&quot;JMD&quot;,&quot;JOD&quot;,&quot;JPY&quot;,&quot;KES&quot;,&quot;KGS&quot;,&quot;KHR&quot;,&quot;KMF&quot;,&quot;KRW&quot;,&quot;KWD&quot;,&quot;KYD&quot;,&quot;KZT&quot;,&quot;LAK&quot;,&quot;LBP&quot;,&quot;LKR&quot;,&quot;LRD&quot;,&quot;LSL&quot;,&quot;LYD&quot;,&quot;MAD&quot;,&quot;MDL&quot;,&quot;MGA&quot;,&quot;MKD&quot;,&quot;MMK&quot;,&quot;MNT&quot;,&quot;MOP&quot;,&quot;MRO&quot;,&quot;MUR&quot;,&quot;MVR&quot;,&quot;MWK&quot;,&quot;MXN&quot;,&quot;MYR&quot;,&quot;MZN&quot;,&quot;NAD&quot;,&quot;NGN&quot;,&quot;NIO&quot;,&quot;NOK&quot;,&quot;NPR&quot;,&quot;NZD&quot;,&quot;OMR&quot;,&quot;PAB&quot;,&quot;PEN&quot;,&quot;PGK&quot;,&quot;PHP&quot;,&quot;PKR&quot;,&quot;PLN&quot;,&quot;PYG&quot;,&quot;QAR&quot;,&quot;RON&quot;,&quot;RSD&quot;,&quot;RUB&quot;,&quot;RWF&quot;,&quot;SAR&quot;,&quot;SBD&quot;,&quot;SCR&quot;,&quot;SEK&quot;,&quot;SGD&quot;,&quot;SHP&quot;,&quot;SLL&quot;,&quot;SOS&quot;,&quot;SRD&quot;,&quot;SSP&quot;,&quot;STD&quot;,&quot;SVC&quot;,&quot;SZL&quot;,&quot;THB&quot;,&quot;TJS&quot;,&quot;TMT&quot;,&quot;TND&quot;,&quot;TOP&quot;,&quot;TRY&quot;,&quot;TTD&quot;,&quot;TWD&quot;,&quot;TZS&quot;,&quot;UAH&quot;,&quot;UGX&quot;,&quot;USD&quot;,&quot;UYU&quot;,&quot;UZS&quot;,&quot;VEF&quot;,&quot;VES&quot;,&quot;VND&quot;,&quot;VUV&quot;,&quot;WST&quot;,&quot;XAF&quot;,&quot;XAG&quot;,&quot;XAU&quot;,&quot;XCD&quot;,&quot;XDR&quot;,&quot;XOF&quot;,&quot;XPD&quot;,&quot;XPF&quot;,&quot;XPT&quot;,&quot;YER&quot;,&quot;ZAR&quot;,&quot;ZMW&quot;,&quot;ZWL&quot;]</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get wallet balance" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/wallet</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="X-Api-Key" elementType="Header">
<stringProp name="Header.name">X-Api-Key</stringProp>
<stringProp name="Header.value">${inkey}</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
<JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Check Balance is Zero" enabled="true">
<stringProp name="cacheKey">true</stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())
if (resp.balance !== 0) {
AssertionResult.setFailureMessage(&quot;Balance is not zero, but: &quot;+ resp.balance);
AssertionResult.setFailure(true)
}
</stringProp>
<stringProp name="scriptLanguage">javascript</stringProp>
</JSR223Assertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[wallet] Get payments" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/payments</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?wal=${walletId}</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="X-Api-Key" elementType="Header">
<stringProp name="Header.name">X-Api-Key</stringProp>
<stringProp name="Header.value">${inkey}</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:105.0) Gecko/20100101 Firefox/105.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check empty payment list" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="2914">[]</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
+487
View File
@@ -0,0 +1,487 @@
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
<hashTree>
<TestFragmentController guiclass="TestFragmentControllerGui" testclass="TestFragmentController" testname="Test Fragment" enabled="false"/>
<hashTree>
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Init Server" enabled="true"/>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to home page" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">false</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
<stringProp name="TestPlan.comments">Detected the start of a redirect chain</stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
<stringProp name="Header.value">1</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
<stringProp name="50554">307</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">40</intProp>
</ResponseAssertion>
<hashTree/>
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract `is_first_install` var" enabled="true">
<stringProp name="RegexExtractor.useHeaders">true</stringProp>
<stringProp name="RegexExtractor.refname">is_first_install</stringProp>
<stringProp name="RegexExtractor.regex">location: (.*)</stringProp>
<stringProp name="RegexExtractor.template">$1$</stringProp>
<stringProp name="RegexExtractor.default">not-first-install</stringProp>
<stringProp name="RegexExtractor.match_number">0</stringProp>
<stringProp name="Sample.scope">all</stringProp>
</RegexExtractor>
<hashTree/>
</hashTree>
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="If First Install" enabled="true">
<stringProp name="IfController.condition">${__groovy(vars.get(&apos;is_first_install&apos;).contains(&quot;first_install&quot;),)}</stringProp>
<boolProp name="IfController.evaluateAll">false</boolProp>
<boolProp name="IfController.useExpression">true</boolProp>
</IfController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Go to first_install" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/first_install</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
<stringProp name="Header.value">1</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Init super user" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin_password&quot;,&quot;password_repeat&quot;:&quot;admin_password&quot;}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/auth/first_install</stringProp>
<stringProp name="HTTPSampler.method">PUT</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/first_install</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="Else (not first install)" enabled="true">
<stringProp name="IfController.condition">${__groovy(!vars.get(&apos;is_first_install&apos;).contains(&quot;first_install&quot;),)}</stringProp>
<boolProp name="IfController.evaluateAll">false</boolProp>
<boolProp name="IfController.useExpression">true</boolProp>
</IfController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Login super user" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin_password&quot;}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/auth</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Get all admin wallets" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/wallets</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Upgrade-Insecure-Requests" elementType="Header">
<stringProp name="Header.name">Upgrade-Insecure-Requests</stringProp>
<stringProp name="Header.value">1</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="Extract admin wallet and keys" enabled="true">
<stringProp name="cacheKey">true</stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="script">var resp = JSON.parse(prev.getResponseDataAsString())[0]
vars.put(&quot;adminWalletId&quot;, resp.id || &quot;no-admin-wallet-id&quot;);
vars.put(&quot;adminWalletKey&quot;, resp.adminkey || &apos;no-adminkey&apos;);
</stringProp>
<stringProp name="scriptLanguage">javascript</stringProp>
</JSR223PostProcessor>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Top up wallet" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">{&quot;amount&quot;:&quot;1000000&quot;,&quot;id&quot;:&quot;${adminWalletId}&quot;}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/admin/api/v1/topup/</stringProp>
<stringProp name="HTTPSampler.method">PUT</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/wallet?&amp;wal=c1daa33fc4014ef69cd1505f14f322c2</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="[server] Logout super user" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">${host}</stringProp>
<stringProp name="HTTPSampler.port">${port}</stringProp>
<stringProp name="HTTPSampler.protocol">${scheme}</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/api/v1/auth/logout</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSamplerProxy>
<hashTree>
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="Referer" elementType="Header">
<stringProp name="Header.name">Referer</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}/</stringProp>
</elementProp>
<elementProp name="Accept-Language" elementType="Header">
<stringProp name="Header.name">Accept-Language</stringProp>
<stringProp name="Header.value">en-US,en;q=0.5</stringProp>
</elementProp>
<elementProp name="Origin" elementType="Header">
<stringProp name="Header.name">Origin</stringProp>
<stringProp name="Header.value">${scheme}://${host}:${port}</stringProp>
</elementProp>
<elementProp name="Content-Type" elementType="Header">
<stringProp name="Header.name">Content-Type</stringProp>
<stringProp name="Header.value">application/json</stringProp>
</elementProp>
<elementProp name="Accept-Encoding" elementType="Header">
<stringProp name="Header.name">Accept-Encoding</stringProp>
<stringProp name="Header.value">gzip, deflate</stringProp>
</elementProp>
<elementProp name="User-Agent" elementType="Header">
<stringProp name="Header.name">User-Agent</stringProp>
<stringProp name="Header.value">Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0</stringProp>
</elementProp>
<elementProp name="Accept" elementType="Header">
<stringProp name="Header.name">Accept</stringProp>
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Check Status Code 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.custom_message"></stringProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<boolProp name="Assertion.assume_success">false</boolProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
File diff suppressed because it is too large Load Diff
+3214
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+278 -100
View File
@@ -7,51 +7,75 @@ import shutil
import signal
import sys
import traceback
from hashlib import sha256
from http import HTTPStatus
from typing import Callable, List
from pathlib import Path
from typing import Callable, List, Optional
from fastapi import FastAPI, Request
from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from loguru import logger
from slowapi import Limiter
from slowapi.util import get_remote_address
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse
from lnbits.core.crud import get_installed_extensions
from lnbits.core.crud import get_dbversions, get_installed_extensions
from lnbits.core.helpers import migrate_extension_database
from lnbits.core.tasks import register_task_listeners
from lnbits.settings import get_wallet_class, set_wallet_class, settings
from lnbits.core.services import websocketUpdater
from lnbits.core.tasks import ( # watchdog_task
killswitch_task,
wait_for_paid_invoices,
)
from lnbits.settings import settings
from lnbits.tasks import (
cancel_all_tasks,
create_permanent_task,
register_invoice_listener,
)
from lnbits.utils.cache import cache
from lnbits.wallets import get_wallet_class, set_wallet_class
from .commands import db_versions, load_disabled_extension_list, migrate_databases
from .core import (
add_installed_extension,
core_app,
core_app_extra,
update_installed_extension_state,
from .commands import migrate_databases
from .core import init_core_routers
from .core.db import core_app_extra
from .core.services import check_admin_settings, check_webpush_settings
from .core.views.api import add_installed_extension
from .core.views.generic import update_installed_extension_state
from .extension_manager import (
Extension,
InstallableExtension,
get_valid_extensions,
version_parse,
)
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 template_renderer
from .middleware import ExtensionsRedirectMiddleware, InstalledExtensionMiddleware
from .middleware import (
CustomGZipMiddleware,
ExtensionsRedirectMiddleware,
InstalledExtensionMiddleware,
add_first_install_middleware,
add_ip_block_middleware,
add_ratelimit_middleware,
)
from .requestvars import g
from .tasks import (
catch_everything_and_restart,
check_pending_payments,
internal_invoice_listener,
invoice_listener,
webhook_handler,
)
def create_app() -> FastAPI:
configure_logger()
app = FastAPI(
title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
title=settings.lnbits_title,
description=(
"API for LNbits, the free and open source bitcoin wallet and "
"accounts system with plugins."
),
version=settings.version,
license_info={
"name": "MIT License",
@@ -59,12 +83,14 @@ def create_app() -> FastAPI:
},
)
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
app.mount(
"/core/static",
StaticFiles(packages=[("lnbits.core", "static")]),
name="core_static",
)
# Allow registering new extensions routes without direct access to the `app` object
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
# register static files
static_path = Path("lnbits", "static")
static = StaticFiles(directory=static_path)
app.mount("/static", static, name="static")
g().base_url = f"http://{settings.host}:{settings.port}"
@@ -72,29 +98,40 @@ def create_app() -> FastAPI:
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
)
# required for SSO login
app.add_middleware(SessionMiddleware, secret_key=settings.auth_secret_key)
# order of these two middlewares is important
app.add_middleware(InstalledExtensionMiddleware)
app.add_middleware(ExtensionsRedirectMiddleware)
register_custom_extensions_path()
add_first_install_middleware(app)
# adds security middleware
add_ip_block_middleware(app)
add_ratelimit_middleware(app)
register_startup(app)
register_routes(app)
register_async_tasks(app)
register_exception_handlers(app)
# Allow registering new extensions routes without direct access to the `app` object
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
register_shutdown(app)
return app
async def check_funding_source() -> None:
original_sigint_handler = signal.getsignal(signal.SIGINT)
def signal_handler(signal, frame):
logger.debug("SIGINT received, terminating LNbits.")
logger.debug(
f"SIGINT received, terminating LNbits. signal: {signal}, frame: {frame}"
)
sys.exit(1)
signal.signal(signal.SIGINT, signal_handler)
@@ -116,17 +153,16 @@ async def check_funding_source() -> None:
break
logger.error(
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
f"The backend for {WALLET.__class__.__name__} isn't "
f"working properly: '{error_message}'",
RuntimeWarning,
)
except:
except Exception as e:
logger.error(f"Error connecting to {WALLET.__class__.__name__}: {e}")
pass
if settings.lnbits_admin_ui and retry_counter == timeout:
logger.warning(
f"Fallback to VoidWallet, because the backend for {WALLET.__class__.__name__} isn't working properly"
)
set_wallet_class("VoidWallet")
set_void_wallet_class()
WALLET = get_wallet_class()
break
else:
@@ -136,34 +172,53 @@ async def check_funding_source() -> None:
signal.signal(signal.SIGINT, original_sigint_handler)
logger.info(
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
logger.success(
f"✔️ Backend {WALLET.__class__.__name__} connected "
f"and with a balance of {balance} msat."
)
def set_void_wallet_class():
logger.warning(
"Fallback to VoidWallet, because the backend for "
f"{settings.lnbits_backend_wallet_class} isn't working properly"
)
set_wallet_class("VoidWallet")
async def check_installed_extensions(app: FastAPI):
"""
Check extensions that have been installed, but for some reason no longer present in the 'lnbits/extensions' directory.
One reason might be a docker-container that was re-created.
The 'data' directory (where the '.zip' files live) is expected to persist state.
Zips that are missing will be re-downloaded.
Check extensions that have been installed, but for some reason no longer present in
the 'lnbits/extensions' directory. One reason might be a docker-container that was
re-created. The 'data' directory (where the '.zip' files live) is expected to
persist state. Zips that are missing will be re-downloaded.
"""
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
await load_disabled_extension_list()
installed_extensions = await build_all_installed_extensions_list()
installed_extensions = await build_all_installed_extensions_list(False)
for ext in installed_extensions:
try:
installed = check_installed_extension(ext)
installed = await check_installed_extension_files(ext)
if not installed:
await restore_installed_extension(app, ext)
logger.info(f"✔️ Successfully re-installed extension: {ext.id}")
logger.info(
"✔️ Successfully re-installed extension: "
f"{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})"
)
logger.info(f"Installed Extensions ({len(installed_extensions)}):")
for ext in installed_extensions:
logger.info(f"{ext.id} ({ext.installed_version})")
async def build_all_installed_extensions_list() -> List[InstallableExtension]:
async def build_all_installed_extensions_list(
include_deactivated: Optional[bool] = True,
) -> List[InstallableExtension]:
"""
Returns a list of all the installed extensions plus the extensions that
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
@@ -176,39 +231,52 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]:
continue
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
release = ext_releases[0] if len(ext_releases) else None
ext_releases = sorted(
ext_releases, key=lambda r: version_parse(r.version), reverse=True
)
release = next((e for e in ext_releases if e.is_version_compatible), None)
if release:
ext_info = InstallableExtension(
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
)
installed_extensions.append(ext_info)
await add_installed_extension(ext_info)
await update_installed_extension_state(ext_id=ext_id, active=True)
return installed_extensions
if include_deactivated:
return installed_extensions
if settings.lnbits_extensions_deactivate_all:
return []
return [
e
for e in installed_extensions
if e.id not in settings.lnbits_deactivated_extensions
]
def check_installed_extension(ext: InstallableExtension) -> bool:
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
if ext.has_installed_version:
return True
zip_files = glob.glob(
os.path.join(settings.lnbits_data_folder, "extensions", "*.zip")
)
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
if f"./{str(ext.zip_path)}" not in zip_files:
ext.download_archive()
await ext.download_archive()
ext.extract_archive()
return False
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
await add_installed_extension(ext)
await update_installed_extension_state(ext_id=ext.id, active=True)
extension = Extension.from_installable_ext(ext)
register_ext_routes(app, extension)
current_version = (await db_versions()).get(ext.id, 0)
current_version = (await get_dbversions()).get(ext.id, 0)
await migrate_extension_database(extension, current_version)
# mount routes for the new version
@@ -219,28 +287,57 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
def register_routes(app: FastAPI) -> None:
"""Register FastAPI routes / LNbits extensions."""
app.include_router(core_app)
app.include_router(core_html_routes)
init_core_routers(app)
for ext in get_valid_extensions():
for ext in get_valid_extensions(False):
try:
register_ext_routes(app, ext)
except Exception as e:
logger.error(str(e))
raise ImportError(
f"Please make sure that the extension `{ext.code}` follows conventions."
)
logger.error(f"Could not load extension `{ext.code}`: {str(e)}")
def register_custom_extensions_path():
if settings.has_default_extension_path:
return
default_ext_path = os.path.join("lnbits", "extensions")
if os.path.isdir(default_ext_path) and len(os.listdir(default_ext_path)) != 0:
logger.warning(
"You are using a custom extensions path, "
+ "but the default extensions directory is not empty. "
+ f"Please clean-up the '{default_ext_path}' directory."
)
logger.warning(
f"You can move the existing '{default_ext_path}' directory to: "
+ f" '{settings.lnbits_extensions_path}/extensions'"
)
sys.path.append(str(Path(settings.lnbits_extensions_path, "extensions")))
sys.path.append(str(Path(settings.lnbits_extensions_path, "upgrades")))
def register_new_ext_routes(app: FastAPI) -> Callable:
# Returns a function that registers new routes for an extension.
# The returned function encapsulates (creates a closure around) the `app` object but does expose it.
# The returned function encapsulates (creates a closure around)
# the `app` object but does expose it.
def register_new_ext_routes_fn(ext: Extension):
register_ext_routes(app, ext)
return register_new_ext_routes_fn
def register_new_ratelimiter(app: FastAPI) -> Callable:
def register_new_ratelimiter_fn():
limiter = Limiter(
key_func=get_remote_address,
default_limits=[
f"{settings.lnbits_rate_limit_no}/{settings.lnbits_rate_limit_unit}"
],
)
app.state.limiter = limiter
return register_new_ratelimiter_fn
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
"""Register FastAPI routes for extension."""
ext_module = importlib.import_module(ext.module_name)
@@ -254,7 +351,10 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
if hasattr(ext_module, f"{ext.code}_static_files"):
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
for s in ext_statics:
app.mount(s["path"], s["app"], s["name"])
static_dir = Path(
settings.lnbits_extensions_path, "extensions", *s["path"].split("/")
)
app.mount(s["path"], StaticFiles(directory=static_dir), s["name"])
if hasattr(ext_module, f"{ext.code}_redirect_paths"):
ext_redirects = getattr(ext_module, f"{ext.code}_redirect_paths")
@@ -274,18 +374,24 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
def register_startup(app: FastAPI):
@app.on_event("startup")
async def lnbits_startup():
try:
# wait till migration is done
await migrate_databases()
# setup admin settings
await check_admin_settings()
await check_webpush_settings()
log_server_info()
# initialize WALLET
set_wallet_class()
try:
set_wallet_class()
except Exception as e:
logger.error(
f"Error initializing {settings.lnbits_backend_wallet_class}: {e}"
)
set_void_wallet_class()
# initialize funding source
await check_funding_source()
@@ -293,11 +399,45 @@ def register_startup(app: FastAPI):
# check extensions after restart
await check_installed_extensions(app)
# register core and extension routes
register_routes(app)
if settings.lnbits_admin_ui:
initialize_server_logger()
except Exception as e:
logger.error(str(e))
raise ImportError("Failed to run 'startup' event.")
def register_shutdown(app: FastAPI):
@app.on_event("shutdown")
async def on_shutdown():
cancel_all_tasks()
# wait a bit to allow them to finish, so that cleanup can run without problems
await asyncio.sleep(0.1)
WALLET = get_wallet_class()
await WALLET.cleanup()
def initialize_server_logger():
super_user_hash = sha256(settings.super_user.encode("utf-8")).hexdigest()
serverlog_queue = asyncio.Queue()
async def update_websocket_serverlog():
while True:
msg = await serverlog_queue.get()
await websocketUpdater(super_user_hash, msg)
create_permanent_task(update_websocket_serverlog)
logger.add(
lambda msg: serverlog_queue.put_nowait(msg),
format=Formatter().format,
)
def log_server_info():
logger.info("Starting LNbits")
logger.info(f"Version: {settings.version}")
@@ -308,9 +448,10 @@ def log_server_info():
logger.info(f"Site title: {settings.lnbits_site_title}")
logger.info(f"Funding source: {settings.lnbits_backend_wallet_class}")
logger.info(f"Data folder: {settings.lnbits_data_folder}")
logger.info(f"Git version: {settings.lnbits_commit}")
logger.info(f"Database: {get_db_vendor_name()}")
logger.info(f"Service fee: {settings.lnbits_service_fee}")
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
def get_db_vendor_name():
@@ -318,29 +459,30 @@ def get_db_vendor_name():
return (
"PostgreSQL"
if db_url and db_url.startswith("postgres://")
else "CockroachDB"
if db_url and db_url.startswith("cockroachdb://")
else "SQLite"
else (
"CockroachDB"
if db_url and db_url.startswith("cockroachdb://")
else "SQLite"
)
)
def register_async_tasks(app):
@app.route("/wallet/webhook")
async def webhook_listener():
return await webhook_handler()
@app.on_event("startup")
async def listeners():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(check_pending_payments))
loop.create_task(catch_everything_and_restart(invoice_listener))
loop.create_task(catch_everything_and_restart(internal_invoice_listener))
await register_task_listeners()
# await run_deferred_async() # calle: doesn't do anyting?
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
@app.on_event("shutdown")
async def stop_listeners():
pass
# core invoice listener
invoice_queue = asyncio.Queue(5)
register_invoice_listener(invoice_queue, "core")
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
# TODO: implement watchdog properly
# create_permanent_task(watchdog_task)
create_permanent_task(killswitch_task)
def register_exception_handlers(app: FastAPI):
@@ -399,6 +541,13 @@ def register_exception_handlers(app: FastAPI):
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
if exc.headers and "token-expired" in exc.headers:
response = RedirectResponse("/")
response.delete_cookie("cookie_access_token")
response.delete_cookie("is_lnbits_user_authorized")
response.set_cookie("is_access_token_expired", "true")
return response
return template_renderer().TemplateResponse(
"error.html",
{
@@ -417,26 +566,55 @@ def configure_logger() -> None:
logger.remove()
log_level: str = "DEBUG" if settings.debug else "INFO"
formatter = Formatter()
logger.add(sys.stderr, level=log_level, format=formatter.format)
logger.add(sys.stdout, level=log_level, format=formatter.format)
if settings.enable_log_to_file:
logger.add(
Path(settings.lnbits_data_folder, "logs", "lnbits.log"),
rotation=settings.log_rotation,
retention=settings.log_retention,
level="INFO",
format=formatter.format,
)
logger.add(
Path(settings.lnbits_data_folder, "logs", "debug.log"),
rotation=settings.log_rotation,
retention=settings.log_retention,
level="DEBUG",
format=formatter.format,
)
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.error").propagate = False
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base").propagate = False
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
class Formatter:
def __init__(self):
self.padding = 0
self.minimal_fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>\n"
self.minimal_fmt = (
"<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 = (
"<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
self.fmt = self.minimal_fmt
def format(self, record):
function = "{function}".format(**record) # pylint: disable=C0209
function = "{function}".format(**record)
if function == "emit": # uvicorn logs
return self.minimal_fmt
return self.fmt
+7 -366
View File
@@ -1,366 +1,7 @@
import hashlib
import re
import time
from decimal import Decimal
from typing import List, NamedTuple, Optional
import bitstring
import secp256k1
from bech32 import CHARSET, bech32_decode, bech32_encode
from ecdsa import SECP256k1, VerifyingKey
from ecdsa.util import sigdecode_string
class Route(NamedTuple):
pubkey: str
short_channel_id: str
base_fee_msat: int
ppm_fee: int
cltv: int
class Invoice:
payment_hash: str
amount_msat: int = 0
description: Optional[str] = None
description_hash: Optional[str] = None
payee: Optional[str] = None
date: int
expiry: int = 3600
secret: Optional[str] = None
route_hints: List[Route] = []
min_final_cltv_expiry: int = 18
def decode(pr: str) -> Invoice:
"""bolt11 decoder,
based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py
"""
hrp, decoded_data = bech32_decode(pr)
if hrp is None or decoded_data is None:
raise ValueError("Bad bech32 checksum")
if not hrp.startswith("ln"):
raise ValueError("Does not start with ln")
bitarray = _u5_to_bitarray(decoded_data)
# final signature 65 bytes, split it off.
if len(bitarray) < 65 * 8:
raise ValueError("Too short to contain signature")
# extract the signature
signature = bitarray[-65 * 8 :].tobytes()
# the tagged fields as a bitstream
data = bitstring.ConstBitStream(bitarray[: -65 * 8])
# build the invoice object
invoice = Invoice()
# decode the amount from the hrp
m = re.search(r"[^\d]+", hrp[2:])
if m:
amountstr = hrp[2 + m.end() :]
if amountstr != "":
invoice.amount_msat = _unshorten_amount(amountstr)
# pull out date
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 or []) / 5
if tag == "d":
invoice.description = _trim_to_bytes(tagdata).decode()
elif tag == "h" and data_length == 52:
invoice.description_hash = _trim_to_bytes(tagdata).hex()
elif tag == "p" and data_length == 52:
invoice.payment_hash = _trim_to_bytes(tagdata).hex()
elif tag == "x":
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
# from the signature
elif tag == "s":
invoice.secret = _trim_to_bytes(tagdata).hex()
elif tag == "r":
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route(
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)
# BOLT #11:
# A reader MUST check that the `signature` is valid (see the `n` tagged
# field specified below).
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
sig = signature[0:64]
if invoice.payee:
key = VerifyingKey.from_string(bytes.fromhex(invoice.payee), curve=SECP256k1)
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
else:
keys = VerifyingKey.from_public_key_recovery(
sig, message, SECP256k1, hashlib.sha256
)
signaling_byte = signature[64]
key = keys[int(signaling_byte)]
invoice.payee = key.to_string("compressed").hex()
return invoice
def encode(options):
"""Convert options into LnAddr and pass it to the encoder"""
addr = LnAddr()
addr.currency = options["currency"]
addr.fallback = options["fallback"] if options["fallback"] else None
if options["amount"]:
addr.amount = options["amount"]
if options["timestamp"]:
addr.date = int(options["timestamp"])
addr.paymenthash = bytes.fromhex(options["paymenthash"])
if options["description"]:
addr.tags.append(("d", options["description"]))
if options["description_hash"]:
addr.tags.append(("h", options["description_hash"]))
if options["expires"]:
addr.tags.append(("x", options["expires"]))
if options["fallback"]:
addr.tags.append(("f", options["fallback"]))
if options["route"]:
for r in options["route"]:
splits = r.split("/")
route = []
while len(splits) >= 5:
route.append(
(
bytes.fromhex(splits[0]),
bytes.fromhex(splits[1]),
int(splits[2]),
int(splits[3]),
int(splits[4]),
)
)
splits = splits[5:]
assert len(splits) == 0
addr.tags.append(("r", route))
return lnencode(addr, options["privkey"])
def lnencode(addr, privkey):
if addr.amount:
amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi.
if amount * 10**12 % 10:
raise ValueError(f"Cannot encode {addr.amount}: too many decimal places")
amount = addr.currency + shorten_amount(amount)
else:
amount = addr.currency if addr.currency else ""
hrp = f"ln{amount}0n"
# Start with the timestamp
data = bitstring.pack("uint:35", addr.date)
# Payment hash
data += tagged_bytes("p", addr.paymenthash)
tags_set = set()
for k, v in addr.tags:
# BOLT #11:
#
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
if k in ("d", "h", "n", "x"):
if k in tags_set:
raise ValueError(f"Duplicate '{k}' tag")
if k == "r":
route = bitstring.BitArray()
for step in v:
pubkey, channel, feebase, feerate, cltv = step
route.append(
bitstring.BitArray(pubkey)
+ bitstring.BitArray(channel)
+ bitstring.pack("intbe:32", feebase)
+ bitstring.pack("intbe:32", feerate)
+ bitstring.pack("intbe:16", cltv)
)
data += tagged("r", route)
elif k == "f":
# NOTE: there was an error fallback here that's now removed
continue
elif k == "d":
data += tagged_bytes("d", v.encode())
elif k == "x":
# Get minimal length by trimming leading 5 bits at a time.
expirybits = bitstring.pack("intbe:64", v)[4:64]
while expirybits.startswith("0b00000"):
expirybits = expirybits[5:]
data += tagged("x", expirybits)
elif k == "h":
data += tagged_bytes("h", v)
elif k == "n":
data += tagged_bytes("n", v)
else:
# FIXME: Support unknown tags?
raise ValueError(f"Unknown tag {k}")
tags_set.add(k)
# BOLT #11:
#
# A writer MUST include either a `d` or `h` field, and MUST NOT include
# both.
if "d" in tags_set and "h" in tags_set:
raise ValueError("Cannot include both 'd' and 'h'")
if "d" not in tags_set and "h" not in tags_set:
raise ValueError("Must include either 'd' or 'h'")
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
privkey = secp256k1.PrivateKey(bytes.fromhex(privkey))
sig = privkey.ecdsa_sign_recoverable(
bytearray([ord(c) for c in hrp]) + data.tobytes()
)
# This doesn't actually serialize, but returns a pair of values :(
sig, recid = privkey.ecdsa_recoverable_serialize(sig)
data += bytes(sig) + bytes([recid])
return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr:
def __init__(
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
self.unknown_tags = []
self.paymenthash = paymenthash
self.signature = None
self.pubkey = None
self.fallback = fallback
self.currency = currency
self.amount = amount
def __str__(self):
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}]]"
def shorten_amount(amount):
"""Given an amount in bitcoin, shorten it"""
# 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
else:
break
return str(amount) + unit
def _unshorten_amount(amount: str) -> int:
"""Given a shortened amount, return millisatoshis"""
# BOLT #11:
# The following `multiplier` letters are defined:
#
# * `m` (milli): multiply by 0.001
# * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001
units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
unit = str(amount)[-1]
# BOLT #11:
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
# anything except a `multiplier` in the table above.
if not re.fullmatch(r"\d+[pnum]?", str(amount)):
raise ValueError(f"Invalid amount '{amount}'")
if unit in units:
return int(int(amount[:-1]) * 100_000_000_000 / units[unit])
else:
return int(amount) * 100_000_000_000
def _pull_tagged(stream):
tag = stream.read(5).uint
length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream)
# Tagged field containing BitArray
def tagged(char, bits):
# Tagged fields need to be zero-padded to 5 bits.
while bits.len % 5 != 0:
bits.append("0b0")
return (
bitstring.pack(
"uint:5, uint:5, uint:5",
CHARSET.find(char),
(bits.len / 5) / 32,
(bits.len / 5) % 32,
)
+ bits
)
def tagged_bytes(char, bits):
return tagged(char, bitstring.BitArray(bits))
def _trim_to_bytes(barr):
# Adds a byte if necessary.
b = barr.tobytes()
if barr.len % 8 != 0:
return b[:-1]
return b
def _readable_scid(short_channel_id: int) -> str:
blockheight = (short_channel_id >> 40) & 0xFFFFFF
transactionindex = (short_channel_id >> 16) & 0xFFFFFF
outputindex = short_channel_id & 0xFFFF
return f"{blockheight}x{transactionindex}x{outputindex}"
def _u5_to_bitarray(arr: List[int]) -> bitstring.BitArray:
ret = bitstring.BitArray()
for a in arr:
ret += bitstring.pack("uint:5", a)
return ret
def bitarray_to_u5(barr):
assert barr.len % 5 == 0
ret = []
s = bitstring.ConstBitStream(barr)
while s.pos != s.len:
ret.append(s.read(5).uint) # type: ignore
return ret
from bolt11 import (
Bolt11 as Invoice, # noqa: F401
)
from bolt11 import (
decode, # noqa: F401
encode, # noqa: F401
)
+549 -29
View File
@@ -1,42 +1,122 @@
import asyncio
import os
import warnings
from functools import wraps
from pathlib import Path
from typing import Optional, Tuple
from urllib.parse import urlparse
import click
import httpx
from fastapi.exceptions import HTTPException
from loguru import logger
from packaging import version
from lnbits.core.models import User
from lnbits.core.services import check_admin_settings
from lnbits.core.views.api import api_install_extension, api_uninstall_extension
from lnbits.settings import settings
from .core import db as core_db
from .core import migrations as core_migrations
from .core.crud import get_dbversions, get_inactive_extensions
from .core.crud import (
delete_accounts_no_wallets,
delete_unused_wallets,
get_dbversions,
get_inactive_extensions,
get_installed_extension,
get_installed_extensions,
remove_deleted_wallets,
)
from .core.helpers import migrate_extension_database, run_migration
from .db import COCKROACH, POSTGRES, SQLITE
from .extension_manager import get_valid_extensions
from .extension_manager import (
CreateExtension,
ExtensionRelease,
InstallableExtension,
get_valid_extensions,
)
@click.command("migrate")
def db_migrate():
def coro(f):
@wraps(f)
def wrapper(*args, **kwargs):
return asyncio.run(f(*args, **kwargs))
return wrapper
@click.group()
def lnbits_cli():
"""
Python CLI for LNbits
"""
@lnbits_cli.group()
def db():
"""
Database related commands
"""
@lnbits_cli.group()
def extensions():
"""
Extensions related commands
"""
def get_super_user() -> Optional[str]:
"""Get the superuser"""
superuser_file = Path(settings.lnbits_data_folder, ".super_user")
if not superuser_file.exists() or not superuser_file.is_file():
raise ValueError(
"Superuser id not found. Please check that the file "
+ f"'{superuser_file.absolute()}' exists and has read permissions."
)
with open(superuser_file, "r") as file:
return file.readline()
@lnbits_cli.command("superuser")
def superuser():
"""Prints the superuser"""
try:
click.echo(get_super_user())
except ValueError as e:
click.echo(str(e))
@lnbits_cli.command("superuser-url")
def superuser_url():
"""Prints the superuser"""
try:
click.echo(
f"http://{settings.host}:{settings.port}/wallet?usr={get_super_user()}"
)
except ValueError as e:
click.echo(str(e))
@lnbits_cli.command("delete-settings")
@coro
async def delete_settings():
"""Deletes the settings"""
async with core_db.connect() as conn:
await conn.execute("DELETE from settings")
@db.command("migrate")
def database_migrate():
"""Migrate databases"""
loop = asyncio.get_event_loop()
loop.run_until_complete(migrate_databases())
async def db_migrate():
asyncio.create_task(migrate_databases())
@click.command("assets")
def handle_assets():
transpile_scss()
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(settings.lnbits_path, "static/scss/base.scss")) as scss:
with open(
os.path.join(settings.lnbits_path, "static/css/base.css"), "w"
) as css:
css.write(compile_string(scss.read()))
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
@@ -48,7 +128,8 @@ async def migrate_databases():
)
elif conn.type in {POSTGRES, COCKROACH}:
exists = await conn.fetchone(
"SELECT * FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'dbversions'"
"SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
" AND table_name = 'dbversions'"
)
if not exists:
@@ -56,22 +137,461 @@ async def migrate_databases():
current_versions = await get_dbversions(conn)
core_version = current_versions.get("core", 0)
await run_migration(conn, core_migrations, core_version)
await run_migration(conn, core_migrations, "core", core_version)
for ext in get_valid_extensions():
# here is the first place we can be sure that the
# `installed_extensions` table has been created
await load_disabled_extension_list()
for ext in get_valid_extensions(False):
current_version = current_versions.get(ext.code, 0)
await migrate_extension_database(ext, current_version)
try:
await migrate_extension_database(ext, current_version)
except Exception as e:
logger.exception(f"Error migrating extension {ext.code}: {e}")
logger.info("✔️ All migrations done.")
@db.command("versions")
@coro
async def db_versions():
"""Show current database versions"""
async with core_db.connect() as conn:
current_versions = await get_dbversions(conn)
return current_versions
click.echo(await get_dbversions(conn))
@db.command("cleanup-wallets")
@click.argument("days", type=int, required=False)
@coro
async def database_cleanup_wallets(days: Optional[int] = None):
"""Delete all wallets that never had any transaction"""
async with core_db.connect() as conn:
delta = days or settings.cleanup_wallets_days
delta = delta * 24 * 60 * 60
await delete_unused_wallets(delta, conn)
@db.command("cleanup-deleted-wallets")
@coro
async def database_cleanup_deleted_wallets():
"""Delete all wallets that has been marked deleted"""
async with core_db.connect() as conn:
await remove_deleted_wallets(conn)
@db.command("cleanup-accounts")
@click.argument("days", type=int, required=False)
@coro
async def database_cleanup_accounts(days: Optional[int] = None):
"""Delete all accounts that have no wallets"""
async with core_db.connect() as conn:
delta = days or settings.cleanup_wallets_days
delta = delta * 24 * 60 * 60
await delete_accounts_no_wallets(delta, conn)
async def load_disabled_extension_list() -> None:
"""Update list of extensions that have been explicitly disabled"""
inactive_extensions = await get_inactive_extensions()
settings.lnbits_deactivated_extensions += inactive_extensions
@extensions.command("list")
@coro
async def extensions_list():
"""Show currently installed extensions"""
click.echo("Installed extensions:")
from lnbits.app import build_all_installed_extensions_list
for ext in await build_all_installed_extensions_list():
assert ext.installed_release, f"Extension {ext.id} has no installed_release"
click.echo(f" - {ext.id} ({ext.installed_release.version})")
@extensions.command("update")
@click.argument("extension", required=False)
@click.option("-a", "--all", is_flag=True, help="Update all extensions.")
@click.option(
"-i", "--repo-index", help="Select the index of the repository to be used."
)
@click.option(
"-s",
"--source-repo",
help="""
Provide the repository URL to be used for updating.
The URL must be one present in `LNBITS_EXTENSIONS_MANIFESTS`
or configured via the Admin UI. This option is required only
if an extension is present in more than one repository.
""",
)
@click.option(
"-u",
"--url",
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
)
@click.option(
"-d",
"--admin-user",
help="Admin user ID (must have permissions to install extensions).",
)
@coro
async def extensions_update(
extension: Optional[str] = None,
all: Optional[bool] = False,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
):
"""
Update extension to the latest version.
If an extension is not present it will be instaled.
"""
if not extension and not all:
click.echo("Extension ID is required.")
click.echo("Or specify the '--all' flag to update all extensions")
return
if extension and all:
click.echo("Only one of extension ID or the '--all' flag must be specified")
return
if url and not _is_url(url):
click.echo(f"Invalid '--url' option value: {url}")
return
if not await _can_run_operation(url):
return
if extension:
await update_extension(extension, repo_index, source_repo, url, admin_user)
return
click.echo("Updating all extensions...")
installed_extensions = await get_installed_extensions()
updated_extensions = []
for e in installed_extensions:
try:
click.echo(f"""{"="*50} {e.id} {"="*(50-len(e.id))} """)
success, msg = await update_extension(
e.id, repo_index, source_repo, url, admin_user
)
if version:
updated_extensions.append(
{"id": e.id, "success": success, "message": msg}
)
except Exception as ex:
click.echo(f"Failed to install extension '{e.id}': {ex}")
if len(updated_extensions) == 0:
click.echo("No extension was updated.")
return
for u in sorted(updated_extensions, key=lambda d: str(d["id"])):
status = "updated to " if u["success"] else "not updated "
click.echo(
f"""'{u["id"]}' {" "*(20-len(str(u["id"])))}"""
+ f""" - {status}: '{u["message"]}'"""
)
@extensions.command("install")
@click.argument("extension")
@click.option(
"-i", "--repo-index", help="Select the index of the repository to be used."
)
@click.option(
"-s",
"--source-repo",
help="""
Provide the repository URL to be used for updating.
The URL must be one present in `LNBITS_EXTENSIONS_MANIFESTS`
or configured via the Admin UI. This option is required only
if an extension is present in more than one repository.
""",
)
@click.option(
"-u",
"--url",
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
)
@click.option(
"-d",
"--admin-user",
help="Admin user ID (must have permissions to install extensions).",
)
@coro
async def extensions_install(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
):
"""Install a extension"""
click.echo(f"Installing {extension}... {repo_index}")
if url and not _is_url(url):
click.echo(f"Invalid '--url' option value: {url}")
return
if not await _can_run_operation(url):
return
await install_extension(extension, repo_index, source_repo, url, admin_user)
@extensions.command("uninstall")
@click.argument("extension")
@click.option(
"-u",
"--url",
help="Use this option to update a running server. Eg: 'http://localhost:5000'.",
)
@click.option(
"-d",
"--admin-user",
help="Admin user ID (must have permissions to install extensions).",
)
@coro
async def extensions_uninstall(
extension: str, url: Optional[str] = None, admin_user: Optional[str] = None
):
"""Uninstall a extension"""
click.echo(f"Uninstalling '{extension}'...")
if url and not _is_url(url):
click.echo(f"Invalid '--url' option value: {url}")
return
if not await _can_run_operation(url):
return
try:
await _call_uninstall_extension(extension, url, admin_user)
click.echo(f"Extension '{extension}' uninstalled.")
except HTTPException as ex:
click.echo(f"Failed to uninstall '{extension}' Error: '{ex.detail}'.")
return False, ex.detail
except Exception as ex:
click.echo(f"Failed to uninstall '{extension}': {str(ex)}.")
return False, str(ex)
def main():
"""main function"""
lnbits_cli()
if __name__ == "__main__":
main()
async def install_extension(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
) -> Tuple[bool, str]:
try:
release = await _select_release(extension, repo_index, source_repo)
if not release:
return False, "No release selected"
data = CreateExtension(
ext_id=extension,
archive=release.archive,
source_repo=release.source_repo,
version=release.version,
)
await _call_install_extension(data, url, admin_user)
click.echo(f"Extension '{extension}' ({release.version}) installed.")
return True, release.version
except HTTPException as ex:
click.echo(f"Failed to install '{extension}' Error: '{ex.detail}'.")
return False, ex.detail
except Exception as ex:
click.echo(f"Failed to install '{extension}': {str(ex)}.")
return False, str(ex)
async def update_extension(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
url: Optional[str] = None,
admin_user: Optional[str] = None,
) -> Tuple[bool, str]:
try:
click.echo(f"Updating '{extension}' extension.")
installed_ext = await get_installed_extension(extension)
if not installed_ext:
click.echo(
f"Extension '{extension}' is not installed. Preparing to install..."
)
return await install_extension(extension, repo_index, source_repo, url)
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
assert (
installed_ext.installed_release
), "Cannot find previously installed release. Please uninstall first."
release = await _select_release(extension, repo_index, source_repo)
if not release:
return False, "No release selected."
if (
release.version == installed_ext.installed_version
and release.source_repo == installed_ext.installed_release.source_repo
):
click.echo(f"Extension '{extension}' already up to date.")
return False, "Already up to date"
click.echo(f"Updating '{extension}' extension to version: {release.version }")
data = CreateExtension(
ext_id=extension,
archive=release.archive,
source_repo=release.source_repo,
version=release.version,
)
await _call_install_extension(data, url, admin_user)
click.echo(f"Extension '{extension}' updated.")
return True, release.version
except HTTPException as ex:
click.echo(f"Failed to update '{extension}' Error: '{ex.detail}.")
return False, ex.detail
except Exception as ex:
click.echo(f"Failed to update '{extension}': {str(ex)}.")
return False, str(ex)
async def _select_release(
extension: str,
repo_index: Optional[str] = None,
source_repo: Optional[str] = None,
) -> Optional[ExtensionRelease]:
all_releases = await InstallableExtension.get_extension_releases(extension)
if len(all_releases) == 0:
click.echo(f"No repository found for extension '{extension}'.")
return None
latest_repo_releases = _get_latest_release_per_repo(all_releases)
if source_repo:
if source_repo not in latest_repo_releases:
click.echo(f"Repository not found: '{source_repo}'")
return None
return latest_repo_releases[source_repo]
if len(latest_repo_releases) == 1:
return latest_repo_releases[list(latest_repo_releases.keys())[0]]
repos = list(latest_repo_releases.keys())
repos.sort()
if not repo_index:
click.echo("Multiple repos found. Please select one using:")
click.echo(" --repo-index option for index of the repo, or")
click.echo(" --source-repo option for the manifest URL")
click.echo("")
click.echo("Repositories: ")
for index, repo in enumerate(repos):
release = latest_repo_releases[repo]
click.echo(f" [{index}] {repo} --> {release.version}")
click.echo("")
return None
if not repo_index.isnumeric() or not 0 <= int(repo_index) < len(repos):
click.echo(f"--repo-index must be between '0' and '{len(repos) - 1}'")
return None
return latest_repo_releases[repos[int(repo_index)]]
def _get_latest_release_per_repo(all_releases):
latest_repo_releases = {}
for release in all_releases:
try:
if not release.is_version_compatible:
continue
# do not remove, parsing also validates
release_version = version.parse(release.version)
if release.source_repo not in latest_repo_releases:
latest_repo_releases[release.source_repo] = release
continue
if release_version > version.parse(
latest_repo_releases[release.source_repo].version
):
latest_repo_releases[release.source_repo] = release
except version.InvalidVersion as ex:
logger.warning(f"Invalid version {release.name}: {ex}")
return latest_repo_releases
async def _call_install_extension(
data: CreateExtension, url: Optional[str], user_id: Optional[str] = None
):
if url:
user_id = user_id or get_super_user()
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{url}/api/v1/extension?usr={user_id}", json=data.dict(), timeout=40
)
resp.raise_for_status()
else:
await api_install_extension(data, User(id="mock_id"))
async def _call_uninstall_extension(
extension: str, url: Optional[str], user_id: Optional[str] = None
):
if url:
user_id = user_id or get_super_user()
async with httpx.AsyncClient() as client:
resp = await client.delete(
f"{url}/api/v1/extension/{extension}?usr={user_id}", timeout=40
)
resp.raise_for_status()
else:
await api_uninstall_extension(extension, User(id="mock_id"))
async def _can_run_operation(url) -> bool:
await check_admin_settings()
if await _is_lnbits_started(url):
if not url:
click.echo("LNbits server is started. Please either:")
click.echo(
f" - use the '--url' option. Eg: --url=http://{settings.host}:{settings.port}"
)
click.echo(
f" - stop the server running at 'http://{settings.host}:{settings.port}'"
)
return False
elif url:
click.echo(
"The option '--url' has been provided,"
+ f" but no server found runnint at '{url}'"
)
return False
return True
async def _is_lnbits_started(url: Optional[str]):
try:
url = url or f"http://{settings.host}:{settings.port}/api/v1/health"
async with httpx.AsyncClient() as client:
await client.get(url)
return True
except Exception:
return False
def _is_url(url):
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except ValueError:
return False
+25 -10
View File
@@ -1,15 +1,30 @@
from fastapi.routing import APIRouter
from fastapi import APIRouter
from lnbits.core.models import CoreAppExtra
from lnbits.db import Database
from .db import core_app_extra, db
from .views.admin_api import admin_router
from .views.api import api_router
from .views.auth_api import auth_router
db = Database("database")
# this compat is needed for usermanager extension
from .views.generic import generic_router, update_user_extension
from .views.node_api import node_router, public_node_router, super_node_router
from .views.public_api import public_router
from .views.tinyurl_api import tinyurl_router
from .views.webpush_api import webpush_router
core_app: APIRouter = APIRouter()
# backwards compatibility for extensions
core_app = APIRouter(tags=["Core"])
core_app_extra: CoreAppExtra = CoreAppExtra()
from .views.admin_api import * # noqa: F401,F403
from .views.api import * # noqa: F401,F403
from .views.generic import * # noqa: F401,F403
from .views.public_api import * # noqa: F401,F403
def init_core_routers(app):
app.include_router(core_app)
app.include_router(generic_router)
app.include_router(public_router)
app.include_router(api_router)
app.include_router(node_router)
app.include_router(super_node_router)
app.include_router(public_node_router)
app.include_router(admin_router)
app.include_router(tinyurl_router)
app.include_router(webpush_router)
app.include_router(auth_router)
+600 -96
View File
@@ -1,26 +1,101 @@
import datetime
import json
from typing import Any, Dict, List, Optional
from time import time
from typing import Any, Dict, List, Literal, Optional
from urllib.parse import urlparse
from uuid import uuid4
from uuid import UUID, uuid4
import shortuuid
from passlib.context import CryptContext
from lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection, Filters
from lnbits.core.db import db
from lnbits.core.models import WalletType
from lnbits.db import DB_TYPE, SQLITE, Connection, Database, Filters, Page
from lnbits.extension_manager import InstallableExtension
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
from lnbits.settings import (
AdminSettings,
EditableSettings,
SuperSettings,
WebPushSettings,
settings,
)
from . import db
from .models import BalanceCheck, Payment, TinyURL, User, Wallet
from .models import (
BalanceCheck,
CreateUser,
Payment,
PaymentFilters,
PaymentHistoryPoint,
TinyURL,
UpdateUserPassword,
User,
UserConfig,
Wallet,
WebPushSubscription,
)
# accounts
# --------
async def create_account(conn: Optional[Connection] = None) -> User:
async def create_user(
data: CreateUser, user_config: Optional[UserConfig] = None
) -> User:
if not settings.new_accounts_allowed:
raise ValueError("Account creation is disabled.")
if await get_account_by_username(data.username):
raise ValueError("Username already exists.")
if data.email and await get_account_by_email(data.email):
raise ValueError("Email already exists.")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user_id = uuid4().hex
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
tsph = db.timestamp_placeholder
now = int(time())
await db.execute(
f"""
INSERT INTO accounts
(id, email, username, pass, extra, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
""",
(
user_id,
data.email,
data.username,
pwd_context.hash(data.password),
json.dumps(dict(user_config)) if user_config else "{}",
now,
now,
),
)
new_account = await get_account(user_id=user_id)
assert new_account, "Newly created account couldn't be retrieved"
return new_account
async def create_account(
conn: Optional[Connection] = None,
user_id: Optional[str] = None,
email: Optional[str] = None,
user_config: Optional[UserConfig] = 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
extra = json.dumps(dict(user_config)) if user_config else "{}"
now = int(time())
await (conn or db).execute(
f"""
INSERT INTO accounts (id, email, extra, created_at, updated_at)
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
""",
(user_id, email, extra, now, now),
)
new_account = await get_account(user_id=user_id, conn=conn)
assert new_account, "Newly created account couldn't be retrieved"
@@ -28,19 +103,176 @@ async def create_account(conn: Optional[Connection] = None) -> User:
return new_account
async def update_account(
user_id: str,
username: Optional[str] = None,
email: Optional[str] = None,
user_config: Optional[UserConfig] = None,
) -> Optional[User]:
user = await get_account(user_id)
assert user, "User not found"
if email:
assert not user.email or email == user.email, "Cannot change email."
account = await get_account_by_email(email)
assert not account or account.id == user_id, "Email already in use."
if username:
assert not user.username or username == user.username, "Cannot change username."
account = await get_account_by_username(username)
assert not account or account.id == user_id, "Username already in exists."
username = user.username or username
email = user.email or email
extra = user_config or user.config
now = int(time())
await db.execute(
f"""
UPDATE accounts SET (username, email, extra, updated_at) =
(?, ?, ?, {db.timestamp_placeholder})
WHERE id = ?
""",
(
username,
email,
json.dumps(dict(extra)) if extra else "{}",
now,
user_id,
),
)
user = await get_user(user_id)
assert user, "Updated account couldn't be retrieved"
return user
async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"SELECT id, email, pass as password FROM accounts WHERE id = ?", (user_id,)
"""
SELECT id, email, username, created_at, updated_at, extra
FROM accounts WHERE id = ?
""",
(user_id,),
)
user = User(**row) if row else None
if user and row["extra"]:
user.config = UserConfig(**json.loads(row["extra"]))
return user
async def delete_accounts_no_wallets(
time_delta: int,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
f"""
DELETE FROM accounts
WHERE NOT EXISTS (
SELECT wallets.id FROM wallets WHERE wallets.user = accounts.id
) AND updated_at < {db.timestamp_placeholder}
""",
(int(time()) - time_delta,),
)
async def get_user_password(user_id: str) -> Optional[str]:
row = await db.fetchone(
"SELECT pass FROM accounts WHERE id = ?",
(user_id,),
)
if not row:
return None
return row[0]
async def verify_user_password(user_id: str, password: str) -> bool:
existing_password = await get_user_password(user_id)
if not existing_password:
return False
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
return pwd_context.verify(password, existing_password)
# todo: , conn: Optional[Connection] = None ??
async def update_user_password(data: UpdateUserPassword) -> Optional[User]:
assert data.password == data.password_repeat, "Passwords do not match."
# old accounts do not have a pasword
if await get_user_password(data.user_id):
assert data.password_old, "Missing old password"
old_pwd_ok = await verify_user_password(data.user_id, data.password_old)
assert old_pwd_ok, "Invalid credentials."
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
now = int(time())
await db.execute(
f"""
UPDATE accounts SET pass = ?, updated_at = {db.timestamp_placeholder}
WHERE id = ?
""",
(
pwd_context.hash(data.password),
now,
data.user_id,
),
)
user = await get_user(data.user_id)
assert user, "Updated account couldn't be retrieved"
return user
async def get_account_by_username(
username: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"""
SELECT id, username, email, created_at, updated_at
FROM accounts WHERE username = ?
""",
(username,),
)
return User(**row) if row else None
async def get_account_by_email(
email: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"""
SELECT id, username, email, created_at, updated_at
FROM accounts WHERE email = ?
""",
(email,),
)
return User(**row) if row else None
async def get_account_by_username_or_email(
username_or_email: str, conn: Optional[Connection] = None
) -> Optional[User]:
user = await get_account_by_username(username_or_email, conn)
if not user:
user = await get_account_by_email(username_or_email, conn)
return user
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
user = await (conn or db).fetchone(
"SELECT id, email FROM accounts WHERE id = ?", (user_id,)
"""
SELECT id, email, username, pass, extra, created_at, updated_at
FROM accounts WHERE id = ?
""",
(user_id,),
)
if user:
@@ -50,9 +282,11 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
)
wallets = await (conn or db).fetchall(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet = wallets.id
), 0) AS balance_msat
FROM wallets
WHERE "user" = ?
WHERE "user" = ? and wallets.deleted = false
""",
(user_id,),
)
@@ -62,12 +296,16 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
return User(
id=user["id"],
email=user["email"],
username=user["username"],
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,
super_user=user["id"] == settings.super_user,
has_password=True if user["pass"] else False,
config=UserConfig(**json.loads(user["extra"])) if user["extra"] else None,
)
@@ -80,19 +318,22 @@ async def add_installed_extension(
conn: Optional[Connection] = None,
) -> None:
meta = {
"installed_release": dict(ext.installed_release)
if ext.installed_release
else None,
"installed_release": (
dict(ext.installed_release) if ext.installed_release else None
),
"dependencies": ext.dependencies,
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
}
version = ext.installed_release.version if ext.installed_release else ""
await (conn or db).execute(
"""
INSERT INTO installed_extensions (id, version, name, short_description, icon, stars, meta) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO
UPDATE SET (version, name, active, short_description, icon, stars, meta) = (?, ?, ?, ?, ?, ?, ?)
INSERT INTO installed_extensions
(id, version, name, short_description, icon, stars, meta)
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET
(version, name, active, short_description, icon, stars, meta) =
(?, ?, ?, ?, ?, ?, ?)
""",
(
ext.id,
@@ -135,13 +376,34 @@ async def delete_installed_extension(
)
async def get_installed_extension(ext_id: str, conn: Optional[Connection] = None):
async def drop_extension_db(*, ext_id: str, conn: Optional[Connection] = None) -> None:
db_version = await (conn or db).fetchone(
"SELECT * FROM dbversions WHERE db = ?", (ext_id,)
)
# Check that 'ext_id' is a valid extension id and not a malicious string
assert db_version, f"Extension '{ext_id}' db version cannot be found"
is_file_based_db = await Database.clean_ext_db_files(ext_id)
if is_file_based_db:
return
# String formatting is required, params are not accepted for 'DROP SCHEMA'.
# The `ext_id` value is verified above.
await (conn or db).execute(
f"DROP SCHEMA IF EXISTS {ext_id} CASCADE",
(),
)
async def get_installed_extension(
ext_id: str, conn: Optional[Connection] = None
) -> Optional[InstallableExtension]:
row = await (conn or db).fetchone(
"SELECT * FROM installed_extensions WHERE id = ?",
(ext_id,),
)
return dict(row) if row else None
return InstallableExtension.from_row(row) if row else None
async def get_installed_extensions(
@@ -185,10 +447,11 @@ async def create_wallet(
conn: Optional[Connection] = None,
) -> Wallet:
wallet_id = uuid4().hex
now = int(time())
await (conn or db).execute(
"""
INSERT INTO wallets (id, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
f"""
INSERT INTO wallets (id, name, "user", adminkey, inkey, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
""",
(
wallet_id,
@@ -196,6 +459,8 @@ async def create_wallet(
user_id,
uuid4().hex,
uuid4().hex,
now,
now,
),
)
@@ -206,15 +471,28 @@ async def create_wallet(
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
wallet_id: str,
name: Optional[str] = None,
currency: Optional[str] = None,
conn: Optional[Connection] = None,
) -> Optional[Wallet]:
set_clause = []
values: list = []
set_clause.append(f"updated_at = {db.timestamp_placeholder}")
now = int(time())
values.append(now)
if name:
set_clause.append("name = ?")
values.append(name)
if currency is not None:
set_clause.append("currency = ?")
values.append(currency)
values.append(wallet_id)
await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
WHERE id = ?
f"""
UPDATE wallets SET {', '.join(set_clause)} WHERE id = ?
""",
(new_name, wallet_id),
tuple(values),
)
wallet = await get_wallet(wallet_id=wallet_id, conn=conn)
assert wallet, "updated created wallet couldn't be retrieved"
@@ -224,16 +502,33 @@ async def update_wallet(
async def delete_wallet(
*, user_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
now = int(time())
await (conn or db).execute(
"""
UPDATE wallets AS w
SET
"user" = 'del:' || w."user",
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
f"""
UPDATE wallets
SET deleted = true, updated_at = {db.timestamp_placeholder}
WHERE id = ? AND "user" = ?
""",
(wallet_id, user_id),
(now, wallet_id, user_id),
)
async def remove_deleted_wallets(conn: Optional[Connection] = None) -> None:
await (conn or db).execute("DELETE FROM wallets WHERE deleted = true")
async def delete_unused_wallets(
time_delta: int,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
f"""
DELETE FROM wallets
WHERE (
SELECT COUNT(*) FROM apipayments WHERE wallet = wallets.id
) = 0 AND updated_at < {db.timestamp_placeholder}
""",
(int(time()) - time_delta,),
)
@@ -242,9 +537,8 @@ async def get_wallet(
) -> Optional[Wallet]:
row = await (conn or db).fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE id = ?
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
AS balance_msat FROM wallets WHERE id = ?
""",
(wallet_id,),
)
@@ -253,13 +547,15 @@ async def get_wallet(
async def get_wallet_for_key(
key: str, key_type: str = "invoice", conn: Optional[Connection] = None
key: str,
key_type: WalletType = WalletType.invoice,
conn: Optional[Connection] = None,
) -> Optional[Wallet]:
row = await (conn or db).fetchone(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE adminkey = ? OR inkey = ?
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0)
AS balance_msat FROM wallets
WHERE (adminkey = ? OR inkey = ?) AND deleted = false
""",
(key, key),
)
@@ -267,7 +563,7 @@ async def get_wallet_for_key(
if not row:
return None
if key_type == "admin" and row["adminkey"] != key:
if key_type == WalletType.admin and row["adminkey"] != key:
return None
return Wallet(**row)
@@ -278,6 +574,11 @@ async def get_total_balance(conn: Optional[Connection] = None):
return 0 if row[0] is None else row[0]
async def get_active_wallet_total_balance(conn: Optional[Connection] = None):
row = await (conn or db).fetchone("SELECT SUM(balance) FROM balances")
return 0 if row[0] is None else row[0]
# wallet payments
# ---------------
@@ -302,6 +603,7 @@ async def get_standalone_payment(
SELECT *
FROM apipayments
WHERE {clause}
ORDER BY amount
LIMIT 1
""",
tuple(values),
@@ -329,7 +631,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
rows = await db.fetchall(
f"""
SELECT * FROM apipayments
WHERE pending = 'false'
WHERE pending = false
AND extra LIKE ?
AND extra LIKE ?
ORDER BY time DESC LIMIT {limit}
@@ -343,7 +645,7 @@ async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: in
return rows
async def get_payments(
async def get_payments_paginated(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
@@ -352,28 +654,23 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[Payment]] = None,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
) -> Page[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
args: List[Any] = []
values: List[Any] = []
clause: List[str] = []
if since is not None:
if db.type == POSTGRES:
clause.append("time > to_timestamp(?)")
elif db.type == COCKROACH:
clause.append("time > cast(? AS timestamp)")
else:
clause.append("time > ?")
args.append(since)
clause.append(f"time > {db.timestamp_placeholder}")
values.append(since)
if wallet_id:
clause.append("wallet = ?")
args.append(wallet_id)
values.append(wallet_id)
if complete and pending:
pass
@@ -397,21 +694,53 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
if not filters:
filters = Filters(limit=None, offset=None)
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{filters.where(clause)}
ORDER BY time DESC
{filters.pagination()}
""",
filters.values(args),
return await (conn or db).fetch_page(
"SELECT * FROM apipayments",
clause,
values,
filters=filters,
model=Payment,
)
return [Payment.from_row(row) for row in rows]
async def get_payments(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> list[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
filters = filters or Filters()
filters.sortby = filters.sortby or "time"
filters.direction = filters.direction or "desc"
filters.limit = limit or filters.limit
filters.offset = offset or filters.offset
page = await get_payments_paginated(
wallet_id=wallet_id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
since=since,
exclude_uncheckable=exclude_uncheckable,
filters=filters,
conn=conn,
)
return page.data
async def delete_expired_invoices(
@@ -449,22 +778,16 @@ async def create_payment(
memo: str,
fee: int = 0,
preimage: Optional[str] = None,
expiry: Optional[datetime.datetime] = None,
pending: bool = True,
extra: Optional[Dict] = None,
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"
try:
invoice = bolt11.decode(payment_request)
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
except:
# assume maximum bolt11 expiry of 31 days to be on the safe side
expiration_date = datetime.datetime.now() + datetime.timedelta(days=31)
# we don't allow the creation of the same invoice twice
# note: this can be removed if the db uniquess constarints are set appropriately
previous_payment = await get_standalone_payment(checking_id, conn=conn)
assert previous_payment is None, "Payment already exists"
await (conn or db).execute(
"""
@@ -483,11 +806,13 @@ async def create_payment(
pending,
memo,
fee,
json.dumps(extra)
if extra and extra != {} and type(extra) is dict
else None,
(
json.dumps(extra)
if extra and extra != {} and isinstance(extra, dict)
else None
),
webhook,
db.datetime_to_timestamp(expiration_date),
db.datetime_to_timestamp(expiry) if expiry else None,
),
)
@@ -514,7 +839,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] = []
@@ -548,7 +872,8 @@ async def update_payment_extra(
) -> None:
"""
Only update the `extra` field for the payment.
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
Old values in the `extra` JSON object will be kept
unless the new `extra` overwrites them.
"""
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
@@ -568,10 +893,77 @@ async def update_payment_extra(
)
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
async def update_pending_payments(wallet_id: str):
pending_payments = await get_payments(
wallet_id=wallet_id,
pending=True,
exclude_uncheckable=True,
)
for payment in pending_payments:
await payment.check_status()
DateTrunc = Literal["hour", "day", "month"]
sqlite_formats = {
"hour": "%Y-%m-%d %H:00:00",
"day": "%Y-%m-%d 00:00:00",
"month": "%Y-%m-01 00:00:00",
}
async def get_payments_history(
wallet_id: Optional[str] = None,
group: DateTrunc = "day",
filters: Optional[Filters] = None,
) -> List[PaymentHistoryPoint]:
if not filters:
filters = Filters()
where = ["(pending = False OR amount < 0)"]
values = []
if wallet_id:
where.append("wallet = ?")
values.append(wallet_id)
if DB_TYPE == SQLITE and group in sqlite_formats:
date_trunc = f"strftime('{sqlite_formats[group]}', time, 'unixepoch')"
elif group in ("day", "hour", "month"):
date_trunc = f"date_trunc('{group}', time)"
else:
raise ValueError(f"Invalid group value: {group}")
transactions = await db.fetchall(
f"""
SELECT {date_trunc} date,
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) income,
SUM(CASE WHEN amount < 0 THEN abs(amount) + abs(fee) ELSE 0 END) spending
FROM apipayments
{filters.where(where)}
GROUP BY date
ORDER BY date DESC
""",
filters.values(values),
)
if wallet_id:
wallet = await get_wallet(wallet_id)
if wallet:
balance = wallet.balance_msat
else:
raise ValueError("Unknown wallet")
else:
balance = await get_total_balance()
# since we dont know the balance at the starting point,
# we take the current balance and walk backwards
results: list[PaymentHistoryPoint] = []
for row in transactions:
results.insert(
0,
PaymentHistoryPoint(
balance=balance, date=row[0], income=row[1], spending=row[2]
),
)
balance -= row.income - row.spending
return results
async def delete_wallet_payment(
@@ -586,6 +978,10 @@ async def delete_wallet_payment(
async def check_internal(
payment_hash: str, conn: Optional[Connection] = None
) -> Optional[str]:
"""
Returns the checking_id of the internal payment if it exists,
otherwise None
"""
row = await (conn or db).fetchone(
"""
SELECT checking_id FROM apipayments
@@ -602,7 +998,10 @@ async def check_internal(
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"""
"""
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
@@ -616,6 +1015,16 @@ async def check_internal_pending(
return row["pending"]
async def mark_webhook_sent(payment_hash: str, status: int) -> None:
await db.execute(
"""
UPDATE apipayments SET webhook_status = ?
WHERE hash = ?
""",
(status, payment_hash),
)
# balance_check
# -------------
@@ -701,20 +1110,27 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
return None
row_dict = dict(sets)
row_dict.pop("super_user")
row_dict.pop("auth_all_methods")
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,
)
return admin_settings
async def delete_admin_settings():
async def delete_admin_settings() -> None:
await db.execute("DELETE FROM settings")
async def update_admin_settings(data: EditableSettings):
await db.execute("UPDATE settings SET editable_settings = ?", (json.dumps(data),))
async def update_admin_settings(data: EditableSettings) -> None:
row = await db.fetchone("SELECT editable_settings FROM settings")
editable_settings = json.loads(row["editable_settings"]) if row else {}
editable_settings.update(data.dict(exclude_unset=True))
await db.execute(
"UPDATE settings SET editable_settings = ?", (json.dumps(editable_settings),)
)
async def update_super_user(super_user: str) -> SuperSettings:
@@ -749,6 +1165,15 @@ async def update_migration_version(conn, db_name, version):
)
async def delete_dbversion(*, ext_id: str, conn: Optional[Connection] = None) -> None:
await (conn or db).execute(
"""
DELETE FROM dbversions WHERE db = ?
""",
(ext_id,),
)
# tinyurl
# -------
@@ -788,3 +1213,82 @@ async def delete_tinyurl(tinyurl_id: str):
"DELETE FROM tiny_url WHERE id = ?",
(tinyurl_id,),
)
# push_notification
# -----------------
async def get_webpush_settings() -> Optional[WebPushSettings]:
row = await db.fetchone("SELECT * FROM webpush_settings")
if not row:
return None
vapid_keypair = json.loads(row["vapid_keypair"])
return WebPushSettings(**vapid_keypair)
async def create_webpush_settings(webpush_settings: dict):
await db.execute(
"INSERT INTO webpush_settings (vapid_keypair) VALUES (?)",
(json.dumps(webpush_settings),),
)
return await get_webpush_settings()
async def get_webpush_subscription(
endpoint: str, user: str
) -> Optional[WebPushSubscription]:
row = await db.fetchone(
"SELECT * FROM webpush_subscriptions WHERE endpoint = ? AND user = ?",
(
endpoint,
user,
),
)
return WebPushSubscription(**dict(row)) if row else None
async def get_webpush_subscriptions_for_user(
user: str,
) -> List[WebPushSubscription]:
rows = await db.fetchall(
"SELECT * FROM webpush_subscriptions WHERE user = ?",
(user,),
)
return [WebPushSubscription(**dict(row)) for row in rows]
async def create_webpush_subscription(
endpoint: str, user: str, data: str, host: str
) -> WebPushSubscription:
await db.execute(
"""
INSERT INTO webpush_subscriptions (endpoint, user, data, host)
VALUES (?, ?, ?, ?)
""",
(
endpoint,
user,
data,
host,
),
)
subscription = await get_webpush_subscription(endpoint, user)
assert subscription, "Newly created webpush subscription couldn't be retrieved"
return subscription
async def delete_webpush_subscription(endpoint: str, user: str) -> None:
await db.execute(
"DELETE FROM webpush_subscriptions WHERE endpoint = ? AND user = ?",
(
endpoint,
user,
),
)
async def delete_webpush_subscriptions(endpoint: str) -> None:
await db.execute(
"DELETE FROM webpush_subscriptions WHERE endpoint = ?", (endpoint,)
)
+5
View File
@@ -0,0 +1,5 @@
from lnbits.core.models import CoreAppExtra
from lnbits.db import Database
db = Database("database")
core_app_extra: CoreAppExtra = CoreAppExtra()
+68 -14
View File
@@ -1,15 +1,16 @@
import importlib
import re
from typing import Any
from typing import Any, Optional
from uuid import UUID
import httpx
from loguru import logger
from lnbits.core.db import db as core_db
from lnbits.db import Connection
from lnbits.extension_manager import Extension
from lnbits.settings import settings
from . import db as core_db
from .crud import update_migration_version
@@ -24,12 +25,13 @@ async def migrate_extension_database(ext: Extension, current_version):
)
async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations, current_version)
await run_migration(ext_conn, ext_migrations, ext.code, current_version)
async def run_migration(db: Connection, migrations_module: Any, current_version: int):
async def run_migration(
db: Connection, migrations_module: Any, db_name: str, current_version: int
):
matcher = re.compile(r"^m(\d\d\d)_")
db_name = migrations_module.__name__.split(".")[-2]
for key, migrate in migrations_module.__dict__.items():
match = matcher.match(key)
if match:
@@ -46,20 +48,72 @@ async def run_migration(db: Connection, migrations_module: Any, current_version:
await update_migration_version(conn, db_name, version)
async def stop_extension_background_work(ext_id: str, user: str):
async def stop_extension_background_work(
ext_id: str, user: str, access_token: Optional[str] = None
):
"""
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
Extensions SHOULD expose a DELETE enpoint at the root level of their API.
This function tries first to call the endpoint using `http` and if if fails it tries using `https`.
Extensions SHOULD expose a `api_stop()` function and/or a DELETE enpoint
at the root level of their API.
"""
stopped = await _stop_extension_background_work(ext_id)
if not stopped:
# fallback to REST API call
await _stop_extension_background_work_via_api(ext_id, user, access_token)
async def _stop_extension_background_work(ext_id) -> bool:
upgrade_hash = settings.extension_upgrade_hash(ext_id) or ""
ext = Extension(ext_id, True, False, upgrade_hash=upgrade_hash)
try:
logger.info(f"Stopping background work for extension '{ext.module_name}'.")
old_module = importlib.import_module(ext.module_name)
# Extensions must expose an `{ext_id}_stop()` function at the module level
# The `api_stop()` function is for backwards compatibility (will be deprecated)
stop_fns = [f"{ext_id}_stop", "api_stop"]
stop_fn_name = next((fn for fn in stop_fns if hasattr(old_module, fn)), None)
assert stop_fn_name, "No stop function found for '{ext.module_name}'"
await getattr(old_module, stop_fn_name)()
logger.info(f"Stopped background work for extension '{ext.module_name}'.")
except Exception as ex:
logger.warning(f"Failed to stop background work for '{ext.module_name}'.")
logger.warning(ex)
return False
return True
async def _stop_extension_background_work_via_api(ext_id, user, access_token):
logger.info(
f"Stopping background work for extension '{ext_id}' using the REST API."
)
async with httpx.AsyncClient() as client:
try:
url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
await client.delete(url)
headers = (
{"Authorization": "Bearer " + access_token} if access_token else None
)
resp = await client.delete(url=url, headers=headers)
resp.raise_for_status()
logger.info(f"Stopped background work for extension '{ext_id}'.")
except Exception as ex:
logger.warning(
f"Failed to stop background work for '{ext_id}' using the REST API."
)
logger.warning(ex)
try:
# try https
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 Exception:
raise ValueError("Invalid hex string for User ID.")
return UUID(hex=user_id[:32], version=4)
+172 -2
View File
@@ -1,4 +1,5 @@
import datetime
from time import time
from loguru import logger
from sqlalchemy.exc import OperationalError
@@ -239,7 +240,8 @@ async def m007_set_invoice_expiries(db):
invoice.date + invoice.expiry
)
logger.info(
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
f" {invoice.payment_hash} to {expiration_date}"
)
await db.execute(
"""
@@ -251,7 +253,7 @@ async def m007_set_invoice_expiries(db):
checking_id,
),
)
except:
except Exception:
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
@@ -301,3 +303,171 @@ async def m010_create_installed_extensions_table(db):
);
"""
)
async def m011_optimize_balances_view(db):
"""
Make the calculation of the balance a single aggregation
over the payments table instead of 2.
"""
await db.execute("DROP VIEW balances")
await db.execute(
"""
CREATE VIEW balances AS
SELECT wallet, SUM(amount - abs(fee)) AS balance
FROM apipayments
WHERE (pending = false AND amount > 0) OR amount < 0
GROUP BY wallet
"""
)
async def m012_add_currency_to_wallet(db):
await db.execute(
"""
ALTER TABLE wallets ADD COLUMN currency TEXT
"""
)
async def m013_add_deleted_to_wallets(db):
"""
Adds deleted column to wallets.
"""
try:
await db.execute(
"ALTER TABLE wallets ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT false"
)
except OperationalError:
pass
async def m014_set_deleted_wallets(db):
"""
Sets deleted column to wallets.
"""
try:
rows = await (
await db.execute(
"""
SELECT *
FROM wallets
WHERE user LIKE 'del:%'
AND adminkey LIKE 'del:%'
AND inkey LIKE 'del:%'
"""
)
).fetchall()
for row in rows:
try:
user = row[2].split(":")[1]
adminkey = row[3].split(":")[1]
inkey = row[4].split(":")[1]
await db.execute(
"""
UPDATE wallets SET user = ?, adminkey = ?, inkey = ?, deleted = true
WHERE id = ?
""",
(user, adminkey, inkey, row[0]),
)
except Exception:
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
# run twice in some environments.
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
async def m015_create_push_notification_subscriptions_table(db):
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
endpoint TEXT NOT NULL,
"user" TEXT NOT NULL,
data TEXT NOT NULL,
host TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
PRIMARY KEY (endpoint, "user")
);
"""
)
async def m016_add_username_column_to_accounts(db):
"""
Adds username column to accounts.
"""
try:
await db.execute("ALTER TABLE accounts ADD COLUMN username TEXT")
await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT")
except OperationalError:
pass
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
"""
Adds created_at and updated_at column to accounts and wallets.
"""
try:
await db.execute(
"ALTER TABLE accounts "
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE accounts "
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE wallets "
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE wallets "
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
# # set their wallets created_at with the first payment
# await db.execute(
# """
# UPDATE wallets SET created_at = (
# SELECT time FROM apipayments
# WHERE apipayments.wallet = wallets.id
# ORDER BY time ASC LIMIT 1
# )
# """
# )
# # then set their accounts created_at with the wallet
# await db.execute(
# """
# UPDATE accounts SET created_at = (
# SELECT created_at FROM wallets
# WHERE wallets.user = accounts.id
# ORDER BY created_at ASC LIMIT 1
# )
# """
# )
# set all to now where they are null
now = int(time())
await db.execute(
f"""
UPDATE wallets SET created_at = {db.timestamp_placeholder}
WHERE created_at IS NULL
""",
(now,),
)
await db.execute(
f"""
UPDATE accounts SET created_at = {db.timestamp_placeholder}
WHERE created_at IS NULL
""",
(now,),
)
except OperationalError as exc:
logger.error(f"Migration 17 failed: {exc}")
pass
+207 -21
View File
@@ -3,28 +3,39 @@ import hashlib
import hmac
import json
import time
from dataclasses import dataclass
from enum import Enum
from sqlite3 import Row
from typing import Callable, Dict, List, Optional
from ecdsa import SECP256k1, SigningKey
from lnurl import encode as lnurl_encode
from fastapi import Query
from loguru import logger
from pydantic import BaseModel
from lnbits.db import Connection
from lnbits.db import Connection, FilterModel, FromRowModel
from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class, settings
from lnbits.wallets.base import PaymentStatus
from lnbits.lnurl import encode as lnurl_encode
from lnbits.settings import settings
from lnbits.wallets import get_wallet_class
from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus
class Wallet(BaseModel):
class BaseWallet(BaseModel):
id: str
name: str
user: str
adminkey: str
inkey: str
balance_msat: int
class Wallet(BaseWallet):
user: str
currency: Optional[str]
deleted: bool
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property
def balance(self) -> int:
return self.balance_msat // 1000
@@ -37,11 +48,10 @@ class Wallet(BaseModel):
@property
def lnurlwithdraw_full(self) -> str:
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
try:
return lnurl_encode(url)
except:
except Exception:
return ""
def lnurlauth_key(self, domain: str) -> SigningKey:
@@ -58,14 +68,47 @@ class Wallet(BaseModel):
return await get_standalone_payment(payment_hash)
class WalletType(Enum):
admin = 0
invoice = 1
invalid = 2
# backwards compatibility
def __eq__(self, other):
return self.value == other
@dataclass
class WalletTypeInfo:
wallet_type: WalletType
wallet: Wallet
class UserConfig(BaseModel):
email_verified: Optional[bool] = False
first_name: Optional[str] = None
last_name: Optional[str] = None
display_name: Optional[str] = None
picture: Optional[str] = None
# Auth provider, possible values:
# - "env": the user was created automatically by the system
# - "lnbits": the user was created via register form (username/pass or user_id only)
# - "google | github | ...": the user was created using an SSO provider
provider: Optional[str] = "lnbits" # auth provider
class User(BaseModel):
id: str
email: Optional[str] = None
username: Optional[str] = None
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
admin: bool = False
super_user: bool = False
has_password: bool = False
config: Optional[UserConfig] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property
def wallet_ids(self) -> List[str]:
@@ -86,7 +129,44 @@ class User(BaseModel):
return False
class Payment(BaseModel):
class CreateUser(BaseModel):
email: Optional[str] = Query(default=None)
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
class UpdateUser(BaseModel):
user_id: str
email: Optional[str] = Query(default=None)
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
config: Optional[UserConfig] = None
class UpdateUserPassword(BaseModel):
user_id: str
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
password_old: Optional[str] = Query(default=None, min_length=8, max_length=50)
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
class LoginUsr(BaseModel):
usr: str
class LoginUsernamePassword(BaseModel):
username: str
password: str
class Payment(FromRowModel):
checking_id: str
pending: bool
amount: int
@@ -169,6 +249,8 @@ class Payment(BaseModel):
async def set_pending(self, pending: bool) -> None:
from .crud import update_payment_status
self.pending = pending
await update_payment_status(self.checking_id, pending)
async def check_status(
@@ -176,10 +258,11 @@ class Payment(BaseModel):
conn: Optional[Connection] = None,
) -> PaymentStatus:
if self.is_uncheckable:
return PaymentStatus(None)
return PaymentPendingStatus()
logger.debug(
f"Checking {'outgoing' if self.is_out else 'incoming'} pending payment {self.checking_id}"
f"Checking {'outgoing' if self.is_out else 'incoming'} "
f"pending payment {self.checking_id}"
)
WALLET = get_wallet_class()
@@ -193,25 +276,59 @@ class Payment(BaseModel):
if self.is_in and status.pending and self.is_expired and self.expiry:
expiration_date = datetime.datetime.fromtimestamp(self.expiry)
logger.debug(
f"Deleting expired incoming pending payment {self.checking_id}: expired {expiration_date}"
f"Deleting expired incoming pending payment {self.checking_id}: "
f"expired {expiration_date}"
)
await self.delete(conn)
# wait at least 15 minutes before deleting failed outgoing payments
elif self.is_out and status.failed:
logger.warning(
f"Deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete(conn)
if self.time + 900 < int(time.time()):
logger.warning(
f"Deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete(conn)
else:
logger.warning(
f"Tried to delete outgoing payment {self.checking_id}: "
"skipping because it's not old enough"
)
elif not status.pending:
logger.info(
f"Marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
f"Marking '{'in' if self.is_in else 'out'}' "
f"{self.checking_id} as not pending anymore: {status}"
)
await self.update_status(status, conn=conn)
return status
async def delete(self, conn: Optional[Connection] = None) -> None:
from .crud import delete_payment
from .crud import delete_wallet_payment
await delete_payment(self.checking_id, conn=conn)
await delete_wallet_payment(self.checking_id, self.wallet_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 PaymentHistoryPoint(BaseModel):
date: datetime.datetime
income: int
spending: int
balance: int
class BalanceCheck(BaseModel):
@@ -224,8 +341,13 @@ class BalanceCheck(BaseModel):
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
def _do_nothing(*_):
pass
class CoreAppExtra:
register_new_ext_routes: Callable
register_new_ext_routes: Callable = _do_nothing
register_new_ratelimiter: Callable
class TinyURL(BaseModel):
@@ -238,3 +360,67 @@ class TinyURL(BaseModel):
@classmethod
def from_row(cls, row: Row):
return cls(**dict(row))
class ConversionData(BaseModel):
from_: str = "sat"
amount: float
to: str = "usd"
class Callback(BaseModel):
callback: str
class DecodePayment(BaseModel):
data: str
class CreateLnurl(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
unit: Optional[str] = None
class CreateInvoice(BaseModel):
unit: str = "sat"
internal: bool = False
out: bool = True
amount: float = Query(None, ge=0)
memo: Optional[str] = None
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
expiry: Optional[int] = None
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
bolt11: Optional[str] = None
class CreateTopup(BaseModel):
id: str
amount: int
class CreateLnurlAuth(BaseModel):
callback: str
class CreateWallet(BaseModel):
name: Optional[str] = None
class CreateWebPushSubscription(BaseModel):
subscription: str
class WebPushSubscription(BaseModel):
endpoint: str
user: str
data: str
host: str
timestamp: str
+349 -64
View File
@@ -1,31 +1,43 @@
import asyncio
import datetime
import json
import time
from io import BytesIO
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypedDict
from urllib.parse import parse_qs, urlparse
import httpx
from bolt11 import Bolt11
from bolt11 import decode as bolt11_decode
from cryptography.hazmat.primitives import serialization
from fastapi import Depends, WebSocket
from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl
from loguru import logger
from py_vapid import Vapid
from py_vapid.utils import b64urlencode
from lnbits import bolt11
from lnbits.core.db import db
from lnbits.db import Connection
from lnbits.decorators import WalletTypeInfo, require_admin_key
from lnbits.helpers import url_for
from lnbits.lnurl import LnurlErrorResponse
from lnbits.lnurl import decode as decode_lnurl
from lnbits.settings import (
FAKE_WALLET,
EditableSettings,
SuperSettings,
get_wallet_class,
readonly_variables,
send_admin_user_to_saas,
settings,
)
from lnbits.wallets.base import PaymentResponse, PaymentStatus
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class
from lnbits.wallets.base import (
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
)
from . import db
from .crud import (
check_internal,
check_internal_pending,
@@ -35,14 +47,19 @@ from .crud import (
create_wallet,
delete_wallet_payment,
get_account,
get_payments,
get_standalone_payment,
get_super_settings,
get_total_balance,
get_wallet,
get_wallet_payment,
update_admin_settings,
update_payment_details,
update_payment_status,
update_super_user,
)
from .models import Payment
from .helpers import to_valid_user_id
from .models import Payment, UserConfig, Wallet
class PaymentFailure(Exception):
@@ -53,10 +70,49 @@ class InvoiceFailure(Exception):
pass
async def calculate_fiat_amounts(
amount: float,
wallet_id: str,
currency: Optional[str] = None,
extra: Optional[Dict] = None,
conn: Optional[Connection] = None,
) -> Tuple[int, Optional[Dict]]:
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet, "invalid wallet_id"
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
if currency and currency != "sat":
amount_sat = await fiat_amount_as_satoshis(amount, currency)
extra = extra or {}
if currency != wallet_currency:
extra["fiat_currency"] = currency
extra["fiat_amount"] = round(amount, ndigits=3)
extra["fiat_rate"] = amount_sat / amount
else:
amount_sat = int(amount)
if wallet_currency:
if wallet_currency == currency:
fiat_amount = amount
else:
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
extra = extra or {}
extra["wallet_fiat_currency"] = wallet_currency
extra["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
extra["wallet_fiat_rate"] = amount_sat / fiat_amount
logger.debug(
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {extra=}"
)
return amount_sat, extra
async def create_invoice(
*,
wallet_id: str,
amount: int, # in satoshis
amount: float,
currency: Optional[str] = "sat",
memo: str,
description_hash: Optional[bytes] = None,
unhashed_description: Optional[bytes] = None,
@@ -66,34 +122,50 @@ 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.")
user_wallet = await get_wallet(wallet_id, conn=conn)
if not user_wallet:
raise InvoiceFailure(f"Could not fetch wallet '{wallet_id}'.")
invoice_memo = None if description_hash else memo
# use the fake wallet if the invoice is for internal use only
wallet = FAKE_WALLET if internal else get_wallet_class()
amount_sat, extra = await calculate_fiat_amounts(
amount, wallet_id, currency=currency, extra=extra, conn=conn
)
if settings.is_wallet_max_balance_exceeded(
user_wallet.balance_msat / 1000 + amount_sat
):
raise InvoiceFailure(
f"Wallet balance cannot exceed "
f"{settings.lnbits_wallet_limit_max_balance} sats."
)
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
amount=amount,
amount=amount_sat,
memo=invoice_memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=expiry or settings.lightning_invoice_expiry,
)
if not ok:
if not ok or not payment_request or not checking_id:
raise InvoiceFailure(error_message or "unexpected backend error.")
invoice = bolt11.decode(payment_request)
invoice = bolt11_decode(payment_request)
amount_msat = amount * 1000
amount_msat = 1000 * amount_sat
await create_payment(
wallet_id=wallet_id,
checking_id=checking_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=amount_msat,
expiry=get_bolt11_expiry(invoice),
memo=memo,
extra=extra,
webhook=webhook,
@@ -114,23 +186,33 @@ async def pay_invoice(
) -> str:
"""
Pay a Lightning invoice.
First, we create a temporary payment in the database with fees set to the reserve fee.
We then check whether the balance of the payer would go negative.
We then attempt to pay the invoice through the backend.
If the payment is successful, we update the payment in the database with the payment details.
First, we create a temporary payment in the database with fees set to the reserve
fee. We then check whether the balance of the payer would go negative.
We then attempt to pay the invoice through the backend. If the payment is
successful, we update the payment in the database with the payment details.
If the payment is unsuccessful, we delete the temporary payment.
If the payment is still in flight, we hope that some other process will regularly check for the payment.
If the payment is still in flight, we hope that some other process
will regularly check for the payment.
"""
invoice = bolt11.decode(payment_request)
fee_reserve_msat = fee_reserve(invoice.amount_msat)
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
try:
invoice = bolt11_decode(payment_request)
except Exception:
raise InvoiceFailure("Bolt11 decoding failed.")
if not invoice.amount_msat or not invoice.amount_msat > 0:
raise InvoiceFailure("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise InvoiceFailure("Amount in invoice is too high.")
await check_wallet_limits(wallet_id, conn, invoice.amount_msat)
async with db.reuse_conn(conn) if conn else db.connect() as conn:
temp_id = invoice.payment_hash
internal_id = f"internal_{invoice.payment_hash}"
if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.")
_, extra = await calculate_fiat_amounts(
invoice.amount_msat / 1000, wallet_id, extra=extra, conn=conn
)
# put all parameters that don't change here
class PaymentKwargs(TypedDict):
@@ -139,6 +221,7 @@ async def pay_invoice(
payment_hash: str
amount: int
memo: str
expiry: Optional[datetime.datetime]
extra: Optional[Dict]
payment_kwargs: PaymentKwargs = PaymentKwargs(
@@ -146,34 +229,55 @@ async def pay_invoice(
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=-invoice.amount_msat,
expiry=get_bolt11_expiry(invoice),
memo=description or invoice.description or "",
extra=extra,
)
# we check if an internal invoice exists that has already been paid (not pending anymore)
# 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)
# 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:
fee_reserve_total_msat = fee_reserve_total(
invoice.amount_msat, internal=True
)
# 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(
new_payment = await create_payment(
checking_id=internal_id,
fee=0,
fee=0 + abs(fee_reserve_total_msat),
pending=False,
conn=conn,
**payment_kwargs,
)
else:
fee_reserve_total_msat = fee_reserve_total(
invoice.amount_msat, internal=False
)
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
try:
await create_payment(
new_payment = await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
fee=-abs(fee_reserve_total_msat),
conn=conn,
**payment_kwargs,
)
@@ -187,13 +291,18 @@ async def pay_invoice(
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:
if (
not internal_checking_id
and wallet.balance_msat > -fee_reserve_total_msat
):
raise PaymentFailure(
f"You must reserve at least ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
" sat) to cover potential routing fees."
)
raise PermissionError("Insufficient balance.")
if internal_checking_id:
service_fee_msat = service_fee(invoice.amount_msat, internal=True)
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
@@ -202,6 +311,7 @@ async def pay_invoice(
await update_payment_status(
checking_id=internal_checking_id, pending=False, conn=conn
)
await send_payment_notification(wallet, new_payment)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -209,6 +319,8 @@ async def pay_invoice(
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
await internal_invoice_queue.put(internal_checking_id)
else:
fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False)
service_fee_msat = service_fee(invoice.amount_msat, internal=False)
logger.debug(f"backend: sending payment {temp_id}")
# actually pay the external invoice
WALLET = get_wallet_class()
@@ -218,7 +330,8 @@ async def pay_invoice(
if payment.checking_id and payment.checking_id != temp_id:
logger.warning(
f"backend sent unexpected checking_id (expected: {temp_id} got: {payment.checking_id})"
f"backend sent unexpected checking_id (expected: {temp_id} got:"
f" {payment.checking_id})"
)
logger.debug(f"backend: pay_invoice finished {temp_id}")
@@ -229,11 +342,20 @@ async def pay_invoice(
await update_payment_details(
checking_id=temp_id,
pending=payment.ok is not True,
fee=payment.fee_msat,
fee=-(
abs(payment.fee_msat if payment.fee_msat else 0)
+ abs(service_fee_msat)
),
preimage=payment.preimage,
new_checking_id=payment.checking_id,
conn=conn,
)
wallet = await get_wallet(wallet_id, conn=conn)
updated = await get_wallet_payment(
wallet_id, payment.checking_id, conn=conn
)
if wallet and updated:
await send_payment_notification(wallet, updated)
logger.debug(f"payment successful {payment.checking_id}")
elif payment.checking_id is None and payment.ok is False:
# payment failed
@@ -247,12 +369,77 @@ async def pay_invoice(
)
else:
logger.warning(
f"didn't receive checking_id from backend, payment may be stuck in database: {temp_id}"
"didn't receive checking_id from backend, payment may be stuck in"
f" database: {temp_id}"
)
# credit service fee wallet
if settings.lnbits_service_fee_wallet and service_fee_msat:
new_payment = await create_payment(
wallet_id=settings.lnbits_service_fee_wallet,
fee=0,
amount=abs(service_fee_msat),
memo="Service fee",
checking_id="service_fee" + temp_id,
payment_request=payment_request,
payment_hash=invoice.payment_hash,
pending=False,
)
return invoice.payment_hash
async def check_wallet_limits(wallet_id, conn, amount_msat):
await check_time_limit_between_transactions(conn, wallet_id)
await check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat)
async def check_time_limit_between_transactions(conn, wallet_id):
limit = settings.lnbits_wallet_limit_secs_between_trans
if not limit or limit <= 0:
return
payments = await get_payments(
since=int(time.time()) - limit,
wallet_id=wallet_id,
limit=1,
conn=conn,
)
if len(payments) == 0:
return
raise ValueError(
f"The time limit of {limit} seconds between payments has been reached."
)
async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat):
limit = settings.lnbits_wallet_limit_daily_max_withdraw
if not limit or limit <= 0:
return
payments = await get_payments(
since=int(time.time()) - 60 * 60 * 24,
outgoing=True,
wallet_id=wallet_id,
limit=1,
conn=conn,
)
if len(payments) == 0:
return
total = 0
for pay in payments:
total += pay.amount
total = total - amount_msat
if limit * 1000 + total < 0:
raise ValueError(
"Daily withdrawal limit of "
+ str(settings.lnbits_wallet_limit_daily_max_withdraw)
+ " sats reached."
)
async def redeem_lnurl_withdraw(
wallet_id: str,
lnurl_request: str,
@@ -266,7 +453,8 @@ async def redeem_lnurl_withdraw(
res = {}
async with httpx.AsyncClient() as client:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
lnurl = decode_lnurl(lnurl_request)
r = await client.get(str(lnurl))
res = r.json()
@@ -279,9 +467,10 @@ async def redeem_lnurl_withdraw(
extra=extra,
conn=conn,
)
except:
except Exception:
logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
f"failed to create invoice on redeem_lnurl_withdraw "
f"from {lnurl}. params: {res}"
)
return None
@@ -299,7 +488,8 @@ async def redeem_lnurl_withdraw(
except Exception:
pass
async with httpx.AsyncClient() as client:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
await client.get(res["callback"], params=params)
except Exception:
@@ -362,7 +552,8 @@ async def perform_lnurlauth(
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
async with httpx.AsyncClient() as client:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
r = await client.get(
callback,
@@ -391,22 +582,56 @@ async def check_transaction_status(
wallet_id, payment_hash, conn=conn
)
if not payment:
return PaymentStatus(None)
return PaymentPendingStatus()
if not payment.pending:
# note: before, we still checked the status of the payment again
return PaymentStatus(True, fee_msat=payment.fee)
return PaymentSuccessStatus(fee_msat=payment.fee)
status: PaymentStatus = await payment.check_status()
return status
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int) -> int:
# WARN: this same value must be used for balance check and passed to
# WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
if internal:
return 0
reserve_min = settings.lnbits_reserve_fee_min
reserve_percent = settings.lnbits_reserve_fee_percent
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
def service_fee(amount_msat: int, internal: bool = False) -> int:
service_fee_percent = settings.lnbits_service_fee
fee_max = settings.lnbits_service_fee_max * 1000
if settings.lnbits_service_fee_wallet:
if internal and settings.lnbits_service_fee_ignore_internal:
return 0
fee_percentage = int(amount_msat / 100 * service_fee_percent)
if fee_max > 0 and fee_percentage > fee_max:
return fee_max
else:
return fee_percentage
else:
return 0
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
return fee_reserve(amount_msat, internal) + service_fee(amount_msat, internal)
async def send_payment_notification(wallet: Wallet, payment: Payment):
await websocketUpdater(
wallet.id,
json.dumps(
{
"wallet_balance": wallet.balance,
"payment": payment.dict(),
}
),
)
async def update_wallet_balance(wallet_id: str, amount: int):
payment_hash, _ = await create_invoice(
wallet_id=wallet_id,
@@ -425,13 +650,16 @@ async def update_wallet_balance(wallet_id: str, amount: int):
async def check_admin_settings():
if settings.super_user:
settings.super_user = to_valid_user_id(settings.super_user).hex
if settings.lnbits_admin_ui:
settings_db = await get_super_settings()
if not settings_db:
# create new settings if table is empty
logger.warning("Settings DB empty. Inserting default settings.")
settings_db = await init_admin_settings(settings.super_user)
logger.warning("Initialized settings from enviroment variables.")
logger.warning("Initialized settings from environment variables.")
if settings.super_user and settings.super_user != settings_db.super_user:
# .env super_user overwrites DB super_user
@@ -439,13 +667,9 @@ async def check_admin_settings():
update_cached_settings(settings_db.dict())
# printing settings for debugging
logger.debug("Admin settings:")
for key, value in settings.dict(exclude_none=True).items():
logger.debug(f"{key}: {value}")
admin_url = f"{settings.lnbits_baseurl}wallet?usr={settings.super_user}"
logger.success(f"✔️ Access super user account at: {admin_url}")
# saving superuser to {data_dir}/.super_user file
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
file.write(settings.super_user)
# callback for saas
if (
@@ -455,14 +679,49 @@ async def check_admin_settings():
):
send_admin_user_to_saas()
account = await get_account(settings.super_user)
if account and account.config and account.config.provider == "env":
settings.first_install = True
logger.success(
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` "
"to get the superuser."
)
async def check_webpush_settings():
if not settings.lnbits_webpush_privkey:
vapid = Vapid()
vapid.generate_keys()
privkey = vapid.private_pem()
assert vapid.public_key, "VAPID public key does not exist"
pubkey = b64urlencode(
vapid.public_key.public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.UncompressedPoint,
)
)
push_settings = {
"lnbits_webpush_privkey": privkey.decode(),
"lnbits_webpush_pubkey": pubkey,
}
update_cached_settings(push_settings)
await update_admin_settings(EditableSettings(**push_settings))
logger.info("Initialized webpush settings with generated VAPID key pair.")
logger.info(f"Pubkey: {settings.lnbits_webpush_pubkey}")
def update_cached_settings(sets_dict: dict):
for key, value in sets_dict.items():
if key not in readonly_variables:
try:
setattr(settings, key, value)
except:
logger.error(f"error overriding setting: {key}, value: {value}")
if key in readonly_variables:
continue
if key not in settings.dict().keys():
continue
try:
setattr(settings, key, value)
except Exception:
logger.warning(f"Failed overriding setting: {key}, value: {value}")
if "super_user" in sets_dict:
setattr(settings, "super_user", sets_dict["super_user"])
@@ -472,8 +731,9 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
if super_user:
account = await get_account(super_user)
if not account:
account = await create_account()
super_user = account.id
account = await create_account(
user_id=super_user, user_config=UserConfig(provider="env")
)
if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id)
@@ -483,12 +743,12 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
class WebsocketConnectionManager:
def __init__(self):
def __init__(self) -> None:
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
async def connect(self, websocket: WebSocket, item_id: str):
logger.debug(f"Websocket connected to {item_id}")
await websocket.accept()
logger.debug(websocket)
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
@@ -505,3 +765,28 @@ websocketManager = WebsocketConnectionManager()
async def websocketUpdater(item_id, data):
return await websocketManager.send_data(f"{data}", item_id)
async def switch_to_voidwallet() -> None:
WALLET = get_wallet_class()
if WALLET.__class__.__name__ == "VoidWallet":
return
set_wallet_class("VoidWallet")
settings.lnbits_backend_wallet_class = "VoidWallet"
async def get_balance_delta() -> Tuple[int, int, int]:
WALLET = get_wallet_class()
total_balance = await get_total_balance()
error_message, node_balance = await WALLET.status()
if error_message:
raise Exception(error_message)
return node_balance - total_balance, node_balance, total_balance
def get_bolt11_expiry(invoice: Bolt11) -> datetime.datetime:
if invoice.expiry:
return datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
else:
# assume maximum bolt11 expiry of 31 days to be on the safe side
return datetime.datetime.now() + datetime.timedelta(days=31)
+37
View File
@@ -0,0 +1,37 @@
"""Keycloak SSO Login Helper
"""
from typing import Optional
import httpx
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
class KeycloakSSO(SSOBase):
"""Class providing login via Keycloak OAuth"""
provider = "keycloak"
scope = ["openid", "email", "profile"]
discovery_url = ""
async def openid_from_response(
self, response: dict, session: Optional["httpx.AsyncClient"] = None
) -> OpenID:
"""Return OpenID from user information provided by Keycloak"""
return OpenID(
email=response.get("email", ""),
provider=self.provider,
id=response.get("sub"),
first_name=response.get("given_name"),
last_name=response.get("family_name"),
display_name=response.get("name"),
picture=response.get("picture"),
)
async def get_discovery_document(self) -> DiscoveryDocument:
"""Get document containing handy urls"""
async with httpx.AsyncClient() as session:
response = await session.get(self.discovery_url)
content = response.json()
return content
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

-43
View File
@@ -1,43 +0,0 @@
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
filteredExtensions: [],
maxStars: 5,
user: null
}
},
mounted() {
this.filteredExtensions = this.g.extensions
},
watch: {
searchTerm(term) {
// Reset the filter
this.filteredExtensions = this.g.extensions
if (term !== '') {
// 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.filteredExtensions.filter(
extensionNameContains(term)
)
}
}
},
created() {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
},
mixins: [windowMixin]
})
-25
View File
@@ -1,25 +0,0 @@
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
disclaimerDialog: {
show: false,
data: {}
},
walletName: ''
}
},
methods: {
createWallet: function () {
LNbits.href.createWallet(this.walletName)
},
processing: function () {
this.$q.notify({
timeout: 0,
message: 'Processing...',
icon: null
})
}
}
})
+127 -41
View File
@@ -4,53 +4,111 @@ from typing import Dict
import httpx
from loguru import logger
from lnbits.tasks import SseListenersDict, register_invoice_listener
from . import db
from .crud import get_balance_notify
from .models import Payment
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
"api_invoice_listeners"
from lnbits.core.crud import (
get_balance_notify,
get_wallet,
get_webpush_subscriptions_for_user,
mark_webhook_sent,
)
from lnbits.core.models import Payment
from lnbits.core.services import (
get_balance_delta,
send_payment_notification,
switch_to_voidwallet,
)
from lnbits.settings import get_wallet_class, settings
from lnbits.tasks import send_push_notification
api_invoice_listeners: Dict[str, asyncio.Queue] = {}
async def register_task_listeners():
async def killswitch_task():
"""
Registers an invoice listener queue for the core tasks.
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions
and fulfill other core tasks such as dispatching webhooks.
killswitch will check lnbits-status repository for a signal from
LNbits and will switch to VoidWallet if the killswitch is triggered.
"""
invoice_paid_queue = asyncio.Queue(5)
# we register invoice_paid_queue to receive all incoming invoices
register_invoice_listener(invoice_paid_queue, "core/tasks.py")
# register a worker that will react to invoices
asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
while True:
WALLET = get_wallet_class()
if settings.lnbits_killswitch and WALLET.__class__.__name__ != "VoidWallet":
with httpx.Client() as client:
try:
r = client.get(settings.lnbits_status_manifest, timeout=4)
r.raise_for_status()
if r.status_code == 200:
ks = r.json().get("killswitch")
if ks and ks == 1:
logger.error(
"Switching to VoidWallet. Killswitch triggered."
)
await switch_to_voidwallet()
except (httpx.RequestError, httpx.HTTPStatusError):
logger.error(
"Cannot fetch lnbits status manifest."
f" {settings.lnbits_status_manifest}"
)
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
async def watchdog_task():
"""
Registers a watchdog which will check lnbits balance and nodebalance
and will switch to VoidWallet if the watchdog delta is reached.
"""
while True:
WALLET = get_wallet_class()
if settings.lnbits_watchdog and WALLET.__class__.__name__ != "VoidWallet":
try:
delta, *_ = await get_balance_delta()
logger.debug(f"Running watchdog task. current delta: {delta}")
if delta + settings.lnbits_watchdog_delta <= 0:
logger.error(f"Switching to VoidWallet. current delta: {delta}")
await switch_to_voidwallet()
except Exception as e:
logger.error("Error in watchdog task", e)
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
"""
This worker dispatches events to all extensions, dispatches webhooks and balance notifys.
This task dispatches events to all api_invoice_listeners,
webhooks, push notifications and balance notifications.
"""
while True:
payment = await invoice_paid_queue.get()
logger.trace("received invoice paid event")
# send information to sse channel
# dispatch api_invoice_listeners
await dispatch_api_invoice_listeners(payment)
# payment notification
wallet = await get_wallet(payment.wallet_id)
if wallet:
await send_payment_notification(wallet, payment)
# dispatch webhook
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
# dispatch balance_notify
url = await get_balance_notify(payment.wallet_id)
if url:
async with httpx.AsyncClient() as client:
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
try:
r = await client.post(url, timeout=4)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
pass
r.raise_for_status()
await mark_webhook_sent(payment.payment_hash, r.status_code)
except httpx.HTTPStatusError as exc:
status_code = exc.response.status_code
await mark_webhook_sent(payment.payment_hash, status_code)
logger.warning(
f"balance_notify returned a bad status_code: {status_code} "
f"while requesting {exc.request.url!r}."
)
logger.warning(exc)
except httpx.RequestError as exc:
await mark_webhook_sent(payment.payment_hash, -1)
logger.warning(f"Could not send balance_notify to {url}")
logger.warning(exc)
# dispatch push notification
await send_payment_push_notification(payment)
async def dispatch_api_invoice_listeners(payment: Payment):
@@ -59,10 +117,12 @@ async def dispatch_api_invoice_listeners(payment: Payment):
"""
for chan_name, send_channel in api_invoice_listeners.items():
try:
logger.debug(f"sending invoice paid event to {chan_name}")
logger.debug(f"api invoice listener: sending paid event to {chan_name}")
send_channel.put_nowait(payment)
except asyncio.QueueFull:
logger.error(f"removing sse listener {send_channel}:{chan_name}")
logger.error(
f"api invoice listener: QueueFull, removing {send_channel}:{chan_name}"
)
api_invoice_listeners.pop(chan_name)
@@ -70,21 +130,47 @@ async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
"""
async with httpx.AsyncClient() as client:
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment.payment_hash, -1)
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
data = payment.dict()
try:
logger.debug("sending webhook", payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
r = await client.post(payment.webhook, json=data, timeout=40)
r.raise_for_status()
await mark_webhook_sent(payment.payment_hash, r.status_code)
except httpx.HTTPStatusError as exc:
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
logger.warning(
f"webhook returned a bad status_code: {exc.response.status_code} "
f"while requesting {exc.request.url!r}."
)
except httpx.RequestError:
await mark_webhook_sent(payment.payment_hash, -1)
logger.warning(f"Could not send webhook to {payment.webhook}")
async def mark_webhook_sent(payment: Payment, status: int) -> None:
await db.execute(
"""
UPDATE apipayments SET webhook_status = ?
WHERE hash = ?
""",
(status, payment.payment_hash),
)
async def send_payment_push_notification(payment: Payment):
wallet = await get_wallet(payment.wallet_id)
if wallet:
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
amount = int(payment.amount / 1000)
title = f"LNbits: {wallet.name}"
body = f"You just received {amount} sat{'s'[:amount^1]}!"
if payment.memo:
body += f"\r\n{payment.memo}"
for subscription in subscriptions:
# todo: review permissions when user-id-only not allowed
# todo: replace all this logic with websockets?
url = (
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
)
await send_push_notification(subscription, title, body, url)
+64 -75
View File
@@ -7,100 +7,89 @@
<div class="col">
<p>Funding Source Info</p>
<ul>
{%raw%}
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
<li>Balance: {{balance / 1000}} sats</li>
{%endraw%}
<li
v-text="'Funding Source: '+ settings.lnbits_backend_wallet_class"
></li>
<li
v-text="'Node Balance: ' + (auditData.node_balance_msats /
1000).toLocaleString() + ' sats'"
></li>
<li
v-text="'LNbits Balance: ' + (auditData.lnbits_balance_msats /
1000).toLocaleString() + ' sats'"
></li>
<li
v-text="'Reserve Percent: ' + (auditData.node_balance_msats /
auditData.lnbits_balance_msats * 100).toFixed(2) + ' %'"
></li>
</ul>
<br />
</div>
<div class="col">
{% if LNBITS_NODE_UI_AVAILABLE %}
<p>Node Management</p>
<q-toggle
label="Node UI"
v-model="formData.lnbits_node_ui"
></q-toggle>
<q-toggle
v-if="formData.lnbits_node_ui"
label="Public node UI"
v-model="formData.lnbits_public_node_ui"
></q-toggle>
<br />
<q-toggle
v-if="formData.lnbits_node_ui"
label="Transactions Tab (Disable on large CLN nodes)"
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
{% else %}
<p>Node Management not supported by active funding source</p>
{% endif %}
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="row">
<div class="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
:disable="!isSuperUser"
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:options="settings.lnbits_allowed_funding_sources"
></q-select>
</div>
</div>
<p>Invoice Expiry</p>
<q-input
filled
v-model.number="formData.lightning_invoice_expiry"
type="number"
label="Invoice expiry (seconds)"
mask="#######"
>
</q-input>
</div>
<div class="col-12 col-md-8">
<p>Fee reserve</p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p>Invoice Expiry</p>
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lightning_invoice_expiry"
label="Invoice expiry (seconds)"
mask="#######"
v-model="formData.lnbits_reserve_fee_min"
label="Reserve fee in msats"
>
</q-input>
</div>
<div class="col-12 col-md-8">
<p>Fee reserve</p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
label="Reserve fee in msats"
>
</q-input>
</div>
<div class="col-6">
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
label="Reserve fee in percent"
step="0.1"
></q-input>
</div>
</div>
<div class="col-6">
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
label="Reserve fee in percent"
step="0.1"
></q-input>
</div>
</div>
</div>
</div>
<div v-if="isSuperUser">
<p class="q-my-md">
Funding Sources<small> (Requires server restart)</small>
</p>
<q-list
v-for="(fund, idx) in settings.lnbits_allowed_funding_sources"
:key="idx"
>
<q-expansion-item
expand-separator
icon="payments"
:label="fund"
v-if="funding_sources.get(fund)"
>
<q-card>
<q-card-section
v-for="([key, prop], i) in Object.entries(funding_sources.get(fund))"
:key="i"
>
<q-input
dense
filled
type="text"
v-model="formData[key]"
:label="prop.label"
class="q-pr-md"
:hint="prop.hint"
></q-input>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
<lnbits-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
</div>
</div>
</q-card-section>
@@ -0,0 +1,404 @@
<q-tab-panel name="security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">Authentication</h6>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.auth_token_expire_minutes"
type="number"
label="Token expire minutes"
hint="Time in minutes until the token expires"
>
</q-input>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-select
filled
v-model="formData.auth_allowed_methods"
multiple
hint="Allowed authorization methods"
label="Select authorization methods"
:options="formData.auth_all_methods"
></q-select>
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('google-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Google Auth</strong>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.google_client_id"
label="Google Client ID"
hint="Make sure thant the authorized redirect URIs contain https://{domain}/api/v1/auth/google/token"
>
</q-input>
</div>
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.google_client_secret"
type="password"
label="Google Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('github-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">GitHub Auth</strong>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.github_client_id"
label="GitHub Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/github/token"
>
</q-input>
</div>
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.github_client_secret"
type="password"
label="GitHub Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('keycloak-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row">
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_discovery_url"
label="Keycloak Discovey URL"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.keycloak_client_id"
label="Keycloak Client ID"
hint="Make sure thant the authorization callback URL is set to https://{domain}/api/v1/auth/keycloak/token"
>
</q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_secret"
type="password"
label="Keycloak Client Secret"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section class="q-pa-none">
<br />
<h6 class="q-my-none" v-text="$t('security_tools')"></h6>
<div>
<div class="row">
<div v-if="serverlogEnabled" class="column" style="width: 100%">
<div
class="col bg-primary"
style="padding-left: 5px; max-height: 20px; color: #fafafa"
v-text="$t('server_logs')"
></div>
<div class="col" style="background-color: #292929">
<q-scroll-area
ref="logScroll"
style="padding: 10px; color: #fafafa; height: 320px"
>
<small v-for="log in logs"
><span v-text="log"></span><br
/></small>
</q-scroll-area>
</div>
</div>
<q-btn
@click="toggleServerLog"
dense
flat
color="primary"
:label="(serverlogEnabled) ? $t('disable_server_log') : $t('enable_server_log')"
></q-btn>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<p v-text="$t('ip_blocker')"></p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
filled
v-model="formBlockedIPs"
@keydown.enter="addBlockedIPs"
type="text"
:label="$t('enter_ip')"
:hint="$t('block_access_hint')"
>
<q-btn
@click="addExtensionsManifest"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="blocked_ip in formData.lnbits_blocked_ips"
:key="blocked_ip"
removable
@remove="removeBlockedIPs(blocked_ip)"
color="primary"
text-color="white"
v-text="blocked_ip"
></q-chip>
</div>
<br />
</div>
<div class="col-6">
<q-input
filled
v-model="formAllowedIPs"
@keydown.enter="addAllowedIPs"
type="text"
:label="$t('enter_ip')"
:hint="$t('allow_access_hint')"
>
<q-btn
@click="addExtensionsManifest"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="allowed_ip in formData.lnbits_allowed_ips"
:key="allowed_ip"
removable
@remove="removeAllowedIPs(allowed_ip)"
color="primary"
text-color="white"
v-text="allowed_ip"
></q-chip>
</div>
<br />
</div>
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('rate_limiter')"></p>
<div class="row q-col-gutter-md">
<div class="col-6">
<q-input
filled
type="number"
v-model.number="formData.lnbits_rate_limit_no"
:label="$t('number_of_requests')"
></q-input>
</div>
<div class="col-6">
<q-select
filled
:options="[$t('second'),$t('minute'),$t('hour')]"
v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('wallet_limiter')"></p>
<div class="row q-col-gutter-md">
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_max_balance"
:label="$t('wallet_max_ballance')"
></q-input>
</div>
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_daily_max_withdraw"
:label="$t('wallet_limit_max_withdraw_per_day')"
></q-input>
</div>
<div class="col-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_wallet_limit_secs_between_trans"
:label="$t('wallet_limit_secs_between_trans')"
></q-input>
</div>
</div>
</div>
</div>
</div>
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_notifications')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_notifications_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notifications"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
<p
v-if="!formData.lnbits_notifications"
v-text="$t('notifications_disabled')"
></p>
<div v-if="formData.lnbits_notifications">
{% include "admin/_tab_security_notifications.html" %}
</div>
<br />
<div>
<p v-text="$t('notification_source')"></p>
<q-input
filled
v-model="formData.lnbits_status_manifest"
type="text"
:label="$t('notification_source_label')"
/>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p v-text="$t('killswitch')"></p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_killswitch')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_killswitch_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_killswitch"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('killswitch_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('killswitch_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_killswitch_interval"
type="number"
/>
</q-item-section>
</q-item>
<br />
<p v-text="$t('watchdog')"></p>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_watchdog_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_interval"
type="number"
/>
</q-item-section>
</q-item>
<q-item disabled tag="label" v-ripple>
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
<q-item-section>
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_delta_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
type="number"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,77 @@
<q-banner v-if="updateAvailable" class="bg-primary text-white">
<q-icon size="28px" name="update"></q-icon>
<span v-text="$t('update_available', {version: statusData.version})"></span>
<template v-slot:action>
<a
class="q-btn"
color="white"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/lnbits/lnbits/releases"
v-text="$t('releases')"
></a>
</template>
</q-banner>
<q-banner v-if="!updateAvailable" class="bg-green text-white">
<q-icon size="28px" name="checkmark"></q-icon>
<span v-text="$t('latest_update', {version: statusData.version})"></span>
</q-banner>
<q-card>
<q-card-section>
<q-table
dense
flat
:data="statusData.notifications"
:columns="statusDataTable.columns"
:no-data-label="$t('no_notifications')"
>
<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"
v-text="col.label"
></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width class="text-center">
<q-icon
v-if="props.row.type === 'update'"
size="18px"
name="update"
color="green"
></q-icon>
<q-icon
v-if="props.row.type === 'warning'"
size="18px"
name="warning"
color="red"
></q-icon>
</q-td>
<q-td
auto-width
key="date"
:props="props"
v-text="formatDate(props.row.date)"
>
</q-td>
<q-td key="message" :props="props"
><span v-text="props.row.message"></span
><a
v-if="props.row.link"
target="_blank"
rel="noopener noreferrer"
:href="props.row.link"
v-text="$t('more')"
></a
></q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
+98 -29
View File
@@ -7,29 +7,55 @@
<div class="col">
<p>Server Info</p>
<ul>
{%raw%}
<li v-if="settings.lnbits_data_folder">
SQlite: {{settings.lnbits_data_folder}}
</li>
<li v-if="settings.lnbits_database_url">
Postgres: {{settings.lnbits_database_url}}
</li>
{%endraw%}
<li
v-if="settings.lnbits_data_folder"
v-text="'SQlite: ' + settings.lnbits_data_folder"
></li>
<li
v-if="settings.lnbits_database_url"
v-text="'Postgres: ' + settings.lnbits_database_url"
></li>
</ul>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Service Fee</p>
<q-input
<p>Allowed currencies</p>
<q-select
filled
type="number"
v-model.number="formData.lnbits_service_fee"
label="Service fee (%)"
step="0.1"
hint="Fee charged per tx (%)"
></q-input>
v-model="formData.lnbits_allowed_currencies"
multiple
hint="Limit the number of available fiat currencies"
label="Allowed currencies"
:options="{{ currencies | safe }}"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Default Accounting Currency</p>
<q-select
filled
v-model="formData.lnbits_default_accounting_currency"
clearable
hint="Default currency for accounting"
label="Currency"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
></q-select>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<q-select
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.code)"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
@@ -54,21 +80,67 @@
<br />
</div>
</div>
<br />
<h6 class="q-my-none">Service Fee</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Admin Extensions</p>
<q-select
<p>Service Fee</p>
<q-input
filled
v-model="formData.lnbits_admin_extensions"
multiple
hint="Extensions only user with admin privileges can use"
label="Admin extensions"
:options="g.extensions.map(e => e.code)"
></q-select>
type="number"
v-model.number="formData.lnbits_service_fee"
label="Service fee (%)"
step="0.1"
hint="Fee charged per tx (%)"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Service fee max</p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
label="Service fee max (sats)"
hint="Max service fee to charge in (sats)"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Fee Wallet</p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
label="Fee wallet (wallet ID)"
hint="Wallet ID to send funds to"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p>Disable Service Fee for Internal Payments</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Disable Fee</q-item-label>
<q-item-label caption
>Disable Service Fee for Internal Lightning
Payments</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_service_fee_ignore_internal"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
<h6 class="q-my-none">Extensions</h6>
<div>
<p>Extension Sources</p>
<q-input
@@ -82,7 +154,6 @@
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
</q-input>
<div>
{%raw%}
<q-chip
v-for="manifestUrl in formData.lnbits_extensions_manifests"
:key="manifestUrl"
@@ -90,10 +161,8 @@
@remove="removeExtensionsManifest(manifestUrl)"
color="primary"
text-color="white"
>
{{ manifestUrl }}
</q-chip>
{%endraw%}
v-text="manifestUrl"
></q-chip>
</div>
<br />
</div>
+14 -3
View File
@@ -31,12 +31,12 @@
v-model="formData.lnbits_site_description"
filled
type="textarea"
hint="Use plain text or raw HTML"
hint="Use plain text, Markdown, or raw HTML"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<p>Default Wallet Name</p>
<q-input
filled
@@ -46,7 +46,7 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<p>Denomination</p>
<q-input
filled
@@ -57,6 +57,17 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-4">
<p>QR code logo</p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
label="https://example.com/image.svg"
hint="URL to logo image in QR code"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
+69 -53
View File
@@ -1,62 +1,78 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">User Management</h6>
<br />
<div>
<p>Admin Users</p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{%raw%}
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
<h6 class="q-my-none q-mb-sm">User Management</h6>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p>Admin Users</p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
label="User ID"
hint="Users with admin privileges"
>
{{ user }}
</q-chip>
{%endraw%}
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
>
<span v-text="user" />
</q-chip>
</div>
<br />
</div>
<br />
</div>
<div>
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
{% raw %}
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
<div class="col-md-6 col-sm-12">
<p>Allowed Users</p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
label="User ID"
hint="Only these users can use LNbits"
>
{{ user }}
</q-chip>
{% endraw %}
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
>
<span v-text="user" />
</q-chip>
</div>
<br />
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Allow creation of new users</q-item-label>
<q-item-label caption
>Allow creation of new users on the index page</q-item-label
>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_allow_new_accounts"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
<br />
</div>
</q-card-section>
</q-tab-panel>
+21 -384
View File
@@ -8,9 +8,9 @@
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges"
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="checkChanges"
@@ -27,9 +27,9 @@
color="primary"
@click="restartServer"
>
<q-tooltip v-if="needsRestart"
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="needsRestart"
@@ -46,7 +46,9 @@
color="primary"
@click="topUpDialog.show = true"
>
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
<q-tooltip>
<span v-text="$t('add_funds_tooltip')"></span>
</q-tooltip>
</q-btn>
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
@@ -59,7 +61,9 @@
@click="deleteSettings"
class="float-right"
>
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
</div>
@@ -88,6 +92,12 @@
@update="val => tab = val.name"
></q-tab>
<q-tab
name="security"
:label="$t('security')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
:label="$t('theme')"
@@ -101,7 +111,8 @@
<q-tab-panels v-model="tab" animated>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html" %} {%
include "admin/_tab_theme.html" %}
include "admin/_tab_security.html" %} {% include
"admin/_tab_theme.html" %}
</q-tab-panels>
</q-form>
</q-card>
@@ -157,379 +168,5 @@
</q-card>
</q-dialog>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
settings: {},
lnbits_theme_options: [
'classic',
'bitcoin',
'flamingo',
'cyber',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
formData: {},
formAddAdmin: '',
formAddUser: '',
formAddExtensionsManifest: '',
isSuperUser: false,
wallet: {},
cancel: {},
topUpDialog: {
show: false
},
tab: 'funding',
needsRestart: false,
funding_sources: new Map([
['VoidWallet', null],
[
'FakeWallet',
{
fake_wallet_secret: {
value: null,
label: 'Secret'
}
}
],
[
'CoreLightningWallet',
{
corelightning_rpc: {
value: null,
label: 'Endpoint'
}
}
],
[
'LndRestWallet',
{
lnd_rest_endpoint: {
value: null,
label: 'Endpoint'
},
lnd_rest_cert: {
value: null,
label: 'Certificate'
},
lnd_rest_macaroon: {
value: null,
label: 'Macaroon'
},
lnd_rest_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
}
}
],
[
'LndWallet',
{
lnd_grpc_endpoint: {
value: null,
label: 'Endpoint'
},
lnd_grpc_cert: {
value: null,
label: 'Certificate'
},
lnd_grpc_port: {
value: null,
label: 'Port'
},
lnd_grpc_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_grpc_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
}
}
],
[
'LnTipsWallet',
{
lntips_api_endpoint: {
value: null,
label: 'Endpoint'
},
lntips_api_key: {
value: null,
label: 'API Key'
}
}
],
[
'LNPayWallet',
{
lnpay_api_endpoint: {
value: null,
label: 'Endpoint'
},
lnpay_api_key: {
value: null,
label: 'API Key'
},
lnpay_wallet_key: {
value: null,
label: 'Wallet Key'
}
}
],
[
'EclairWallet',
{
eclair_url: {
value: null,
label: 'URL'
},
eclair_pass: {
value: null,
label: 'Password'
}
}
],
[
'LNbitsWallet',
{
lnbits_endpoint: {
value: null,
label: 'Endpoint'
},
lnbits_key: {
value: null,
label: 'Admin Key'
}
}
],
[
'OpenNodeWallet',
{
opennode_api_endpoint: {
value: null,
label: 'Endpoint'
},
opennode_key: {
value: null,
label: 'Key'
}
}
],
[
'ClicheWallet',
{
cliche_endpoint: {
value: null,
label: 'Endpoint'
}
}
],
[
'SparkWallet',
{
spark_url: {
value: null,
label: 'Endpoint'
},
spark_token: {
value: null,
label: 'Token'
}
}
]
])
}
},
created: function () {
this.getSettings()
this.balance = +'{{ balance|safe }}'
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
}
},
methods: {
addAdminUser() {
let addUser = this.formAddAdmin
let admin_users = this.formData.lnbits_admin_users
if (addUser && addUser.length && !admin_users.includes(addUser)) {
//admin_users = [...admin_users, addUser]
this.formData.lnbits_admin_users = [...admin_users, addUser]
this.formAddAdmin = ''
//console.log(this.checkChanges)
}
},
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addAllowedUser() {
let addUser = this.formAddUser
let allowed_users = this.formData.lnbits_allowed_users
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
this.formAddUser = ''
}
},
removeAllowedUser(user) {
let allowed_users = this.formData.lnbits_allowed_users
this.formData.lnbits_allowed_users = allowed_users.filter(
u => u !== user
)
},
addExtensionsManifest() {
const addManifest = this.formAddExtensionsManifest.trim()
const manifests = this.formData.lnbits_extensions_manifests
if (
addManifest &&
addManifest.length &&
!manifests.includes(addManifest)
) {
this.formData.lnbits_extensions_manifests = [
...manifests,
addManifest
]
this.formAddExtensionsManifest = ''
}
},
removeExtensionsManifest(manifest) {
const manifests = this.formData.lnbits_extensions_manifests
this.formData.lnbits_extensions_manifests = manifests.filter(
m => m !== manifest
)
},
restartServer() {
LNbits.api
.request('GET', '/admin/api/v1/restart/?usr=' + this.g.user.id)
.then(response => {
this.$q.notify({
type: 'positive',
message: 'Success! Restarted Server',
icon: null
})
this.needsRestart = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
topupWallet() {
LNbits.api
.request(
'PUT',
'/admin/api/v1/topup/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
this.wallet
)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Added ' +
this.wallet.amount +
' to ' +
this.wallet.id,
icon: null
})
this.wallet = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateFundingData() {
this.settings.lnbits_allowed_funding_sources.map(f => {
let opts = this.funding_sources.get(f)
if (!opts) return
Object.keys(opts).forEach(e => {
opts[e].value = this.settings[e]
})
})
},
getSettings() {
LNbits.api
.request(
'GET',
'/admin/api/v1/settings/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey
)
.then(response => {
this.isSuperUser = response.data.super_user || false
this.settings = response.data
this.formData = _.clone(this.settings)
this.updateFundingData()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
updateSettings() {
let data = _.omit(this.formData, [
'super_user',
'lnbits_allowed_funding_sources'
])
LNbits.api
.request(
'PUT',
'/admin/api/v1/settings/?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
data
)
.then(response => {
this.needsRestart =
this.settings.lnbits_backend_wallet_class !==
this.formData.lnbits_backend_wallet_class
this.settings = this.formData
this.formData = _.clone(this.settings)
this.updateFundingData()
this.$q.notify({
type: 'positive',
message: 'Success! Settings changed!',
icon: null
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteSettings() {
LNbits.utils
.confirmDialog(
'Are you sure you want to restore settings to default?'
)
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/admin/api/v1/settings/?usr=' + this.g.user.id
)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Restored settings to defaults, restart required!',
icon: null
})
this.needsRestart = true
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
downloadBackup() {
window.open('/admin/api/v1/backup/?usr=' + this.g.user.id, '_blank')
}
}
})
</script>
<script src="{{ static_url_for('static', 'js/admin.js') }}"></script>
{% endblock %}
+3 -2
View File
@@ -1,10 +1,11 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
:label="$t('api_docs')"
icon="vpn_key"
:label="$t('api_keys_api_docs')"
:content-inset-level="0.5"
>
<q-card-section>
<strong>Node URL: </strong><em v-text="origin"></em><br />
<strong>Wallet ID: </strong><em>{{ wallet.id }}</em><br />
<strong>Admin key: </strong><em>{{ wallet.adminkey }}</em><br />
<strong>Invoice/read key: </strong><em>{{ wallet.inkey }}</em>
+436
View File
@@ -0,0 +1,436 @@
{% extends "base.html" %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/account.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md">
<div v-if="user" class="col-12 col-md-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row">
<div class="col">
<q-tabs v-model="tab" align="justify">
<q-tab
name="user"
:label="$t('account_settings')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
:label="$t('look_and_feel')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
<q-tab-panels v-model="tab">
<q-tab-panel name="user">
<div v-if="passwordData.show">
<q-card-section>
<div class="row">
<div class="col">
<h4 class="q-my-none">
<span v-text="$t('password_config')"></span>
</h4>
</div>
<div class="col">
<q-img
v-if="user.config.picture"
style="max-width: 100px"
:src="user.config.picture"
class="float-right"
></q-img>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-input
v-if="user.has_password"
v-model="passwordData.oldPassword"
type="password"
autocomplete="off"
label="Old Password"
filled
dense
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
v-model="passwordData.newPassword"
type="password"
autocomplete="off"
:label="$t('password')"
filled
dense
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
v-model="passwordData.newPasswordRepeat"
type="password"
autocomplete="off"
:label="$t('password_repeat')"
filled
dense
class="q-mb-md"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
</q-card-section>
<q-separator></q-separator>
<q-card-section class="q-pb-lg">
<q-btn
@click="updatePassword"
:disable="(!passwordData.newPassword || !passwordData.newPasswordRepeat) || passwordData.newPassword !== passwordData.newPasswordRepeat"
unelevated
color="primary"
:label="$t('change_password')"
>
</q-btn>
<q-btn
@click="passwordData.show = false"
:label="$t('back')"
outline
unelevated
color="grey"
class="float-right"
></q-btn>
</q-card-section>
</div>
<div v-else>
<q-card-section>
<div class="row">
<div class="col">
<q-img
v-if="user.config.picture"
style="max-width: 100px"
:src="user.config.picture"
class="float-right"
></q-img>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-input
v-model="user.id"
:label="$t('user_id')"
filled
dense
readonly
:type="showUserId ? 'text': 'password'"
class="q-mb-md"
><q-btn
@click="showUserId = !showUserId"
dense
flat
:icon="showUserId ? 'visibility_off' : 'visibility'"
color="grey"
></q-btn>
</q-input>
<q-input
v-model="user.username"
:label="$t('username')"
filled
dense
:readonly="hasUsername"
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.email"
:label="$t('email')"
filled
dense
readonly
class="q-mb-md"
>
</q-input>
<div v-if="!user.email" class="row"></div>
<div v-if="!user.email" class="row">
{% if "google-auth" in LNBITS_AUTH_METHODS or
"github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm text-h6">
<span v-text="$t('verify_email')"></span>:
</div>
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<q-btn
:href="`/api/v1/auth/google?user_id=${user.id}`"
type="a"
outline
no-caps
rounded
color="grey"
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/google-logo.png') }}'"
></q-img>
</q-avatar>
<div>Google</div>
</q-btn>
</div>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<q-btn
:href="`/api/v1/auth/github?user_id=${user.id}`"
type="a"
outline
no-caps
color="grey"
rounded
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/github-logo.png') }}'"
></q-img>
</q-avatar>
<div>GitHub</div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>
<q-card-section v-if="user.config">
<q-input
v-model="user.config.first_name"
:label="$t('first_name')"
filled
dense
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.config.last_name"
:label="$t('last_name')"
filled
dense
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.config.provider"
:label="$t('auth_provider')"
filled
dense
readonly
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.config.picture"
:label="$t('picture')"
filled
dense
class="q-mb-md"
>
</q-input>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
<q-btn
@click="showChangePassword()"
:label="user.has_password ? $t('change_password'): $t('set_password')"
outline
unelevated
color="grey"
class="float-right"
></q-btn>
</q-card-section>
</div>
</q-tab-panel>
<q-tab-panel name="theme">
<div class="row q-mb-md">
<div class="col-4"><span v-text="$t('language')"></span></div>
<div class="col-8">
<q-btn-dropdown
dense
flat
round
size="sm"
icon="language"
class="q-pl-md"
>
<q-list v-for="(lang, index) in g.langs" :key="index">
<q-item
clickable
v-close-popup
:active="activeLanguage(lang.value)"
@click="changeLanguage(lang.value)"
><q-item-section>
<q-item-label
v-text="lang.display ?? lang.value.toUpperCase()"
></q-item-label>
<q-tooltip
><span v-text="lang.label"></span
></q-tooltip>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('color_scheme')"></span>
</div>
<div class="col-8">
<q-btn
v-if="g.allowedThemes.includes('classic')"
dense
flat
@click="changeColor('classic')"
icon="circle"
color="deep-purple"
size="md"
><q-tooltip>classic</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeColor('bitcoin')"
icon="circle"
color="orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('mint')"
dense
flat
@click="changeColor('mint')"
icon="circle"
color="green"
size="md"
><q-tooltip>mint</q-tooltip> </q-btn
><q-btn
v-if="g.allowedThemes.includes('autumn')"
dense
flat
@click="changeColor('autumn')"
icon="circle"
color="brown"
size="md"
><q-tooltip>autumn</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('monochrome')"
dense
flat
@click="changeColor('monochrome')"
icon="circle"
color="grey"
size="md"
><q-tooltip>monochrome</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('salvador')"
dense
flat
@click="changeColor('salvador')"
icon="circle"
color="blue-10"
size="md"
><q-tooltip>elSalvador</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('freedom')"
dense
flat
@click="changeColor('freedom')"
icon="circle"
color="pink-13"
size="md"
><q-tooltip>freedom</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('cyber')"
dense
flat
@click="changeColor('cyber')"
icon="circle"
color="light-green-9"
size="md"
><q-tooltip>cyber</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('flamingo')"
dense
flat
@click="changeColor('flamingo')"
icon="circle"
color="pink-3"
size="md"
><q-tooltip>flamingo</q-tooltip>
</q-btn>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('toggle_darkmode')"></span>
</div>
<div class="col-8">
<q-btn
dense
flat
round
@click="toggleDarkMode"
:icon="($q.dark.isActive) ? 'brightness_3' : 'wb_sunny'"
size="sm"
>
<q-tooltip
><span v-text="$t('toggle_darkmode')"></span
></q-tooltip>
</q-btn>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">Notifications</div>
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
pubkey="{{ WEBPUSH_PUBKEY }}"
></lnbits-notifications-btn>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('payment_reactions')"></span>
</div>
<div class="col-8">
<q-select
v-model="reactionChoice"
:options="reactionOptions"
label="Reactions"
@input="reactionChoiceFunc"
>
<q-tooltip
><span v-text="$t('payment_reactions')"></span
></q-tooltip>
</q-select>
</div>
</div>
</q-tab-panel>
</q-tab-panels>
</div>
</div>
</q-card-section>
</q-card>
</div>
<div v-else class="col-12 col-md-6 q-gutter-y-md">
<q-card>
<q-card-section>
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center main">
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section class="grid">
<div>
<h6 class="q-my-none text-center">
<strong>Welcome to LNbits</strong>
<p>Set up the Superuser account below.</p>
</h6>
<br />
<q-form class="q-gutter-md">
<q-input
filled
v-model="loginData.username"
:label="$t('username')"
></q-input>
<q-input
filled
v-model.trim="loginData.password"
:type="loginData.isPwd ? 'password' : 'text'"
autocomplete="off"
:label="$t('password')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-input
filled
v-model.trim="loginData.passwordRepeat"
:type="loginData.isPwdRepeat ? 'password' : 'text'"
autocomplete="off"
:label="$t('password_repeat')"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || 'Passwords_dont_match']"
><template v-slot:append>
<q-icon
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
color="primary"
:label="$t('login')"
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
></q-btn>
</q-form>
</div>
<div class="hero-wrapper">
<div class="hero q-mx-auto"></div>
</div>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
}
.grid {
display: block;
}
.hero-wrapper {
display: none;
}
.hero {
display: block;
height: 100%;
max-width: 250px;
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
@media (min-width: 992px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
.hero-wrapper {
display: block;
position: relative;
height: 100%;
padding: 1rem;
}
}
</style>
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
loginData: {
isPwd: true,
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
}
}
},
created() {
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
},
computed: {
checkPasswordsMatch() {
return this.loginData.password !== this.loginData.passwordRepeat
}
},
methods: {
async setPassword() {
try {
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat
})
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
}
})
</script>
{% endblock %}
+452 -102
View File
@@ -1,74 +1,343 @@
{% extends "public.html" %} {% block scripts %}
<script src="/core/static/js/index.js"></script>
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
{% if lnurl %}
<q-btn
unelevated
color="primary"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
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='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
></q-input>
<q-btn
unelevated
color="primary"
:disable="walletName == ''"
type="submit"
:label="$t('add_wallet')"
></q-btn>
</q-form>
{% endif %}
</q-card-section>
</q-card>
<div
v-if="isUserAuthorized"
class="col-12 col-md-6 col-lg-6 q-gutter-y-md"
></div>
<div v-else class="col-12 col-md-4 col-lg-4 q-gutter-y-md">
<div class="gt-sm">
<h3 class="q-my-none" v-if="'{{SITE_TITLE}}' == 'LNbits'">
{{SITE_TITLE}}
</h3>
<h5 class="q-my-md" v-if="'{{SITE_TITLE}}' == 'LNbits'">
{{SITE_TAGLINE}}
</h5>
</div>
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only" in
LNBITS_AUTH_METHODS)%}
<div class="row q-mt-xl">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="primary"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
v-text="$t('press_to_claim')"
class="full-width"
></q-btn>
</q-card-section>
</q-card>
</div>
</div>
{%else%} {% endif %}
<q-card>
<q-card-section>
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
<p v-text="$t('lnbits_description')"></p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
color="grey"
type="a"
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
:label="$t('view_github')"
></q-btn>
<q-btn
outline
color="grey"
type="a"
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
:label="$t('donate')"
></q-btn>
<div class="row q-mt-md">
<div class="col-12 col-md-10 col-lg-10 q-gutter-y-md">
<q-badge v-if="isAccessTokenExpired" color="primary" rounded>
<div class="text-h5">
<span v-text="$t('session_has_expired')"></span>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION | safe}}</p>
</q-card-section>
</q-card>
</q-badge>
<q-card class="shadow-12">
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
<q-card-section>
<div class="text-h6">
<span v-text="$t('instant_access_question')"></span>
<br />
<q-badge
@click="showLogin('user-id-only')"
color="primary"
class="cursor-pointer"
rounded
>
<strong>
<q-icon name="account_circle" size="xs"></q-icon>
<span v-text="$t('login_with_user_id')"></span> </strong
></q-badge>
{% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
<span v-text="$t('or')"></span>
<q-badge
@click="showRegister('user-id-only')"
color="primary"
class="cursor-pointer"
rounded
>
<strong>
<q-icon name="add" size="xs"></q-icon>
<span v-text="$t('create_new_wallet')"></span>
</strong>
</q-badge>
{%endif%}
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section
v-if="authAction === 'login' && authMethod === 'user-id-only'"
>
<b> <span v-text="$t('login_with_user_id')"></span> </b><br /><br />
<q-form @submit="loginUsr" class="q-gutter-md">
<q-input
filled
dense
v-model="usr"
label="usr"
type="password"
></q-input>
<div>
<q-btn
unelevated
color="primary"
:disable="usr == ''"
type="submit"
label="Login"
class="full-width"
></q-btn>
</div>
</q-form>
</q-card-section>
{%endif%} {% if "username-password" in LNBITS_AUTH_METHODS %}
<q-card-section
v-if="authAction === 'login' && authMethod === 'username-password'"
>
<div class="q-mb-lg">
<strong>
<span v-text="$t('login_to_account')"></span>
</strong>
</div>
<q-form @submit="login" class="q-gutter-md">
<q-input
filled
dense
v-model="username"
name="username"
:label="$t('username_or_email') + ' *'"
></q-input>
<q-input
filled
dense
v-model="password"
name="password"
:label="$t('password') + ' *'"
type="password"
></q-input>
<div>
<q-btn
:disable="!username || !password"
color="primary"
type="submit"
:label="$t('login')"
class="full-width"
></q-btn>
</div>
</q-form>
</q-card-section>
<q-card-section
v-if="authAction === 'register' && authMethod === 'username-password'"
>
<b> <span v-text="$t('create_account')"></span> </b><br /><br />
<q-form @submit="register" class="q-gutter-md">
<q-input
filled
dense
required
v-model="username"
:label="$t('username') + ' *'"
:rules="[(val) => validateUsername(val) || $t('invalid_username')]"
></q-input>
<q-input
filled
dense
v-model="password"
:label="$t('password') + ' *'"
type="password"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<q-input
filled
dense
v-model="passwordRepeat"
:label="$t('password_repeat') + ' *'"
type="password"
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<div>
<q-btn
unelevated
color="primary"
:disable="!password || !passwordRepeat|| !username || (password !== passwordRepeat)"
type="submit"
class="full-width"
:label="$t('create_account')"
></q-btn>
</div>
</q-form>
</q-card-section>
{%endif%} {% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
<q-card-section
v-if="authAction === 'register' && authMethod === 'user-id-only'"
>
<div>
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
filled
dense
v-model="walletName"
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
></q-input>
<div>
<q-btn
color="primary"
:disable="walletName == ''"
type="submit"
:label="$t('add_wallet')"
class="full-width"
></q-btn>
</div>
</q-form>
</div>
</q-card-section>
{% if "username-password" in LNBITS_AUTH_METHODS %}
<q-card-section
v-if="authAction === 'login' && authMethod === 'username-password'"
>
<div>
<q-btn
color="grey"
outline
:label="$t('register')"
class="full-width"
@click="showRegister('username-password')"
></q-btn>
</div>
</q-card-section>
{%endif%}
<q-separator></q-separator>
{% endif %}
<q-card-section
v-if="authAction === 'login' && authMethod === 'username-password'"
>
<div class="row">
{% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/google"
type="a"
outline
no-caps
rounded
color="grey"
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/google-logo.png') }}'"
></q-img>
</q-avatar>
<div>
<span v-text="$t('signin_with_google')"></span>
</div>
</q-btn>
</div>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/github"
type="a"
outline
no-caps
color="grey"
rounded
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/github-logo.png') }}'"
></q-img>
</q-avatar>
<div><span v-text="$t('signin_with_github')"></span></div>
</q-btn>
</div>
{%endif%} {% if "keycloak-auth" in LNBITS_AUTH_METHODS %}
<div class="col-12 full-width q-pa-sm">
<q-btn
href="/api/v1/auth/keycloak"
type="a"
outline
no-caps
color="grey"
rounded
class="full-width"
>
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="'{{ static_url_for('static', 'images/keycloak-logo.png') }}'"
></q-img>
</q-avatar>
<div><span v-text="$t('signin_with_keycloak')"></span></div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>
<q-card-section v-else>
<div class="row">
<div class="col q-pa-sm">
<q-btn
@click="showLogin('username-password')"
:label="$t('back')"
outline
rounded
color="grey"
class="full-width"
>
</q-btn>
</div>
</div>
</q-card-section>
</q-card>
</div>
</div>
</div>
<!-- Ads -->
<div class="col-12 col-md-3 col-lg-3" v-if="'{{SITE_TITLE}}' == 'LNbits'">
<div
v-if="'{{SITE_TITLE}}' != 'LNbits'"
class="col-12 col-md-5 col-lg-5 q-pt-xl"
>
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-html="formatDescription"></div>
</div>
<div class="col-12 col-md-3 col-lg-3 gt-sm" v-else>
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn
outline
color="grey"
type="a"
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener noreferrer"
:label="$t('view_github')"
class="full-width"
></q-btn>
<q-btn
outline
color="grey"
type="a"
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener noreferrer"
:label="$t('donate')"
class="full-width q-mb-lg"
></q-btn>
</div>
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn
flat
@@ -78,18 +347,26 @@
></q-btn>
<div class="row">
<div class="col">
<a href="https://github.com/ElementsProject/lightning">
<a
href="https://github.com/ElementsProject/lightning"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/cln.png' : '/static/images/clnl.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a href="https://github.com/lightningnetwork/lnd">
<a
href="https://github.com/lightningnetwork/lnd"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/lnd.png' : '/static/images/lnd.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
></q-img>
</a>
</div>
@@ -97,18 +374,26 @@
<div class="row">
<div class="col">
<a href="https://opennode.com">
<a
href="https://opennode.com"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/opennode.png' : '/static/images/opennodel.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a href="https://lnpay.co/">
<a
href="https://lnpay.co/"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/lnpay.png' : '/static/images/lnpayl.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
></q-img>
</a>
</div>
@@ -116,60 +401,125 @@
<div class="row">
<div class="col">
<a href="https://github.com/rootzoll/raspiblitz">
<a
href="https://github.com/rootzoll/raspiblitz"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/blitz.png' : '/static/images/blitzl.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a href="https://getumbrel.com/">
<a
href="https://start9.com/"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/umbrel.png' : '/static/images/umbrell.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<a href="https://mynodebtc.com">
<a
href="https://getumbrel.com/"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/mynode.png' : '/static/images/mynodel.png'"
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a
href="https://mynodebtc.com"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">&nbsp;</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://github.com/shesek/spark-wallet"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a
href="https://voltage.cloud"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<a
href="https://getalby.com"
target="_blank"
rel="noopener noreferrer"
>
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
></q-img>
</a>
</div>
<div class="col q-pl-md">
<a href="https://zbd.gg" target="_blank" rel="noopener noreferrer">
<q-img
contain
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
></q-img>
</a>
</div>
</div>
</div>
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="secondary" class="full-width q-mb-md"
>{{ AD_TITLE }}</q-btn
>
<a href="{{ AD[0] }}" class="q-ma-md">
<img
v-if="($q.dark.isActive)"
src="{{ AD[1] }}"
style="max-width: 90%"
/>
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
</a>
</div>
{% endfor %} {% endif %}
</div>
</div>
{% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="secondary" class="full-width q-mb-md"
>{{ AD_SPACE_TITLE }}</q-btn
>
<a href="{{ AD[0] }}" class="q-ma-md">
<img v-if="($q.dark.isActive)" src="{{ AD[1] }}" style="max-width: 90%" />
<img v-else src="{{ AD[2] }}" style="max-width: 90%" />
</a>
</div>
{% endfor %} {% endif %}
</div>
<div v-if="'{{SITE_TITLE}}' == 'LNbits'" class="row gt-sm q-mt-xl">
<div class="col-1"></div>
<div class="col-10 q-pl-xl">
<span v-text="$t('lnbits_description')"></span>
</div>
<div class="col-1"></div>
</div>
{% endblock %}
-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 %}
+299 -172
View File
@@ -3,7 +3,7 @@
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="/core/static/js/wallet.js"></script>
<script src="{{ static_url_for('static', 'js/wallet.js') }}"></script>
{% endblock %}
<!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@@ -15,46 +15,58 @@
{% elif HIDE_API %}
<div class="col-12 q-gutter-y-md">
{% else %}
<div class="col-12 col-md-7 q-gutter-y-md">
<div
class="col-12 col-md-7 q-gutter-y-md"
:style="$q.screen.lt.md ? {
position: mobileSimple ? 'fixed !important': ''
, top: mobileSimple ? '50% !important': ''
, left: mobileSimple ? '50% !important': ''
, transform: mobileSimple ? 'translate(-50%, -50%) !important': ''
} : ''"
>
{% endif %}
<q-card>
<q-card
:style="$q.screen.lt.md ? {
background: $q.screen.lt.md ? 'none !important': ''
, boxShadow: $q.screen.lt.md ? 'none !important': ''
, margin: $q.screen.lt.md && mobileSimple ? 'auto !important': ''
, width: $q.screen.lt.md && mobileSimple ? '90% !important': ''
} : ''"
>
<q-card-section>
<h3 class="q-my-none">
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
{{LNBITS_DENOMINATION}}
<q-btn
<h3 class="q-my-none text-no-wrap">
<strong v-text="formattedBalance"></strong>
<small>{{LNBITS_DENOMINATION}}</small>
<lnbits-update-balance
v-if="'{{user.super_user}}' == 'True'"
:wallet_id="this.g.wallet.id"
flat
:callback="updateBalanceCallback"
round
color="primary"
icon="add"
size="md"
>
<q-popup-edit
class="bg-accent text-white"
v-slot="scope"
v-model="credit"
>
<q-input
filled
:label='$t("credit_label", { denomination: "{{LNBITS_DENOMINATION}}"})'
:hint="$t('credit_hint')"
v-model="scope.value"
dense
autofocus
:mask="'{{LNBITS_DENOMINATION}}' != 'sats' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
:step="'{{LNBITS_DENOMINATION}}' != 'sats' ? '0.01' : '1'"
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
</q-popup-edit>
</q-btn>
/>
</h3>
<div class="row">
<div class="col">
<div v-if="g.wallet.currency">
<span
class="text-h5 text-italic"
v-text="formattedFiatBalance"
style="opacity: 0.75"
></span>
</div>
</div>
<div class="col">
<q-btn
@click="mobileSimple = !mobileSimple"
color="primary"
class="float-right lt-md"
size="sm"
flat
:icon="mobileSimple ? 'unfold_more' : 'unfold_less'"
:label="mobileSimple ? $t('more') : $t('less')"
></q-btn>
</div>
</div>
</q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
<div class="col">
@@ -84,14 +96,20 @@
:label="$t('scan')"
>
<q-tooltip
>{% raw %}{{$t('camera_tooltip')}}{% endraw %}</q-tooltip
>
><span v-text="$t('camera_tooltip')"></span
></q-tooltip>
</q-btn>
</div>
</div>
</q-card>
<q-card>
<q-card
:style="$q.screen.lt.md ? {
background: $q.screen.lt.md ? 'none !important': ''
, boxShadow: $q.screen.lt.md ? 'none !important': ''
, marginTop: $q.screen.lt.md ? '0px !important': ''
} : ''"
>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
@@ -100,16 +118,13 @@
:v-text="$t('transactions')"
></h5>
</div>
<div class="col-auto">
<div class="gt-sm col-auto">
<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>-->
<q-btn
dense
flat
@@ -118,18 +133,20 @@
color="grey"
@click="showChart"
>
<q-tooltip
>{% raw %}{{$t('chart_tooltip')}}{% endraw %}</q-tooltip
>
<q-tooltip>
<span v-text="$t('chart_tooltip')"></span
></q-tooltip>
</q-btn>
</div>
</div>
<q-input
v-if="payments.length > 10"
:style="$q.screen.lt.md ? {
display: mobileSimple ? 'none !important': ''
} : ''"
filled
dense
clearable
v-model="paymentsTable.filter"
v-model="paymentsTable.search"
debounce="300"
:placeholder="$t('search_by_tag_memo_amount')"
class="q-mb-md"
@@ -138,20 +155,26 @@
<q-table
dense
flat
:data="filteredPayments"
:data="paymentsOmitter"
:row-key="paymentTableRowKey"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
:no-data-label="$t('no_transactions')"
:filter="paymentsTable.filter"
:filter="paymentsTable.search"
:loading="paymentsTable.loading"
:hide-header="mobileSimple"
:hide-bottom="mobileSimple"
@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
>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
v-text="col.label"
></q-th>
</q-tr>
</template>
<template v-slot:body="props">
@@ -170,11 +193,13 @@
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>{{$t('pending')}}</q-tooltip>
<q-tooltip
><span v-text="$t('pending')"></span
></q-tooltip>
</q-icon>
</q-td>
<q-td
key="memo"
key="time"
:props="props"
style="white-space: normal; word-break: break-all"
>
@@ -184,38 +209,47 @@
text-color="black"
>
<a
v-text="'#'+props.row.tag"
class="inherit"
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>
:href="['/', props.row.tag].join('')"
></a>
</q-badge>
{{ props.row.memo }}
<span v-text="props.row.memo"></span>
<br />
<i>
<span v-text="props.row.dateFrom"></span>
<q-tooltip
><span v-text="props.row.date"></span
></q-tooltip>
</i>
</q-td>
<q-td auto-width key="date" :props="props">
<q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }}
</q-td>
{% endraw %}
<q-td
auto-width
key="sat"
key="amount"
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
:props="props"
>{% raw %} {{
parseFloat(String(props.row.fsat).replaceAll(",", "")) / 100
}}
v-text="parseFloat(String(props.row.fsat).replaceAll(',', '')) / 100"
>
</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">
<span v-text="props.row.fsat"></span>
<br />
<i v-if="props.row.extra.wallet_fiat_currency">
<span
v-text="formatFiat(props.row.extra.wallet_fiat_currency, props.row.extra.wallet_fiat_amount)"
></span>
<br />
</i>
<i v-if="props.row.extra.fiat_currency">
<span
v-text="formatFiat(props.row.extra.fiat_currency, props.row.extra.fiat_amount)"
></span>
</i>
</q-td>
</q-tr>
<q-dialog v-model="props.expand" :props="props">
<q-dialog v-model="props.expand" :props="props" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
@@ -230,11 +264,9 @@
>
<a :href="'lightning:' + props.row.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
<lnbits-qrcode
:value="'lightning:' + props.row.bolt11.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
></lnbits-qrcode>
</q-responsive>
</a>
</div>
@@ -287,30 +319,28 @@
</q-card>
</q-dialog>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
{% else %}
<div class="col-12 col-md-5 q-gutter-y-md">
<div
v-if="!mobileSimple || $q.screen.gt.sm"
class="col-12 col-md-5 q-gutter-y-md"
>
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet:
<strong><em>{{ wallet.name }}</em></strong>
<strong><em>{{wallet.name}}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "core/_api_docs.html" %}
<q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %}
<q-expansion-item
group="extras"
@@ -320,12 +350,11 @@
<q-card>
<q-card-section class="text-center">
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="lightning:{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
<lnbits-qrcode
:value="lightning:{{wallet.lnurlwithdraw_full}}"
></lnbits-qrcode>
</a>
<p v-text="$('drain_funds_desc')"></p>
<p v-text="$t('drain_funds_desc')"></p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -341,7 +370,7 @@
<q-card-section class="text-center">
<p v-text="$t('export_to_phone_desc')"></p>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:value="'{{request.base_url}}wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</q-card-section>
@@ -358,23 +387,54 @@
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="newName"
label="Label"
dense="dense"
v-model.trim="update.name"
label="Name"
dense
@update:model-value="(e) => console.log(e)"
/>
</div>
<q-btn
:disable="!newName.length"
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_name')"
@click="updateWalletName()"
@click="updateWallet({ name: update.name })"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="attach_money"
:label="$t('fiat_tracking')"
>
<q-card>
<q-card-section>
<div style="max-width: 360px">
<q-select
filled
dense
clearable
v-model="update.currency"
type="text"
:label="$t('currency')"
:options="receive.units.filter((u) => u !== 'sat')"
></q-select>
</div>
<q-btn
:disable="!update.name.length"
unelevated
class="q-mt-sm"
color="primary"
:label="$t('update_currency')"
@click="updateWallet({ currency: update.currency || '' })"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
@@ -387,12 +447,15 @@
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
@click="deleteWallet()"
:label="$t('delete_wallet')"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% include "core/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
@@ -425,22 +488,21 @@
</div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %}
<q-dialog v-model="receive.show" position="top">
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
<b v-text="receive.lnurl.domain"></b> is requesting an invoice:
</p>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
{% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.number="receive.data.amount"
:label="$('amount')+' ({{LNBITS_DENOMINATION}}) *'"
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
reverse-fill-mask
@@ -479,7 +541,6 @@
v-model.trim="receive.data.memo"
:label="$t('memo')"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
@@ -487,9 +548,10 @@
:disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
<span v-if="receive.lnurl">
{{$t('withdraw_from')}} {{receive.lnurl.domain}}
</span>
<span
v-if="receive.lnurl"
v-text="$t('withdraw_from') + receive.lnurl.domain"
></span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
<q-btn
@@ -507,15 +569,16 @@
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-card
v-else-if="receive.paymentReq && receive.lnurl == null"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
<lnbits-qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
></lnbits-qrcode>
</q-responsive>
</a>
</div>
@@ -535,28 +598,32 @@
></q-btn>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-dialog v-model="parse.show" @hide="closeParseDialog" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {%
raw %}
</h6>
<h6
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
class="q-my-none"
v-text="parseFloat(String(parse.invoice.fsat).replaceAll(',', '')) / 100 + '{{LNBITS_DENOMINATION}}'"
></h6>
<h6
v-else
class="q-my-none"
v-text="parse.invoice.fsat + '{{LNBITS_DENOMINATION}}'"
></h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong v-text="$t('description')">:</strong> {{
parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
<strong v-text="$t('memo') + ': '"></strong>
<span v-text="parse.invoice.description"></span>
<br />
<strong>Expire date: </strong>
<span v-text="parse.invoice.expireDate"></span>
<br />
<strong>Hash: </strong>
<span v-text="parse.invoice.hash"></span>
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn
unelevated
@@ -590,24 +657,31 @@
</div>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
Authenticate with <b v-text="parse.lnurlauth.domain"></b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair
will be deterministically generated so your identity can't be
tied to your LNbits wallet or linked across websites. No other
data will be shared with {{ parse.lnurlauth.domain }}.
data will be shared with
<span v-text="parse.lnurlauth.domain"></span>.
</p>
<p>
Your public key for <b v-text="parse.lnurlauth.domain"></b> is:
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
<code class="text-wrap" v-text="parse.lnurlauth.pubkey"></code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn
unelevated
color="primary"
type="submit"
:label="$t('login')"
></q-btn>
<q-btn
:label="$t('cancel')"
v-close-popup
@@ -617,55 +691,74 @@
></q-btn>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }}
{{LNBITS_DENOMINATION}}
<b v-text="parse.lnurlpay.domain"></b> is requesting
<span
v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"
></span>
<span v-text="'{{LNBITS_DENOMINATION}}'"></span>
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
and a
<span v-text="parse.lnurlpay.commentAllowed"></span>-char
comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b>
<b
v-text="parse.lnurlpay.targetUser || parse.lnurlpay.domain"
></b>
is requesting <br />
between
<b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<b v-text="msatoshiFormat(parse.lnurlpay.minSendable)"></b> and
<b v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"></b>
<span v-text="'{{LNBITS_DENOMINATION}}'"></span>
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
and a
<span v-text="parse.lnurlpay.commentAllowed"></span>-char
comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p
class="col text-justify text-italic"
v-text="parse.lnurlpay.description"
></p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-select
filled
dense
v-if="!parse.lnurlpay.fixed"
v-model="parse.data.unit"
type="text"
:label="$t('unit')"
:options="receive.units"
></q-select>
<br />
<q-input
ref="setAmount"
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
fill-mask="0"
reverse-fill-mask
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
:readonly="parse.lnurlpay && parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div
class="col-8 q-pl-md"
@@ -682,9 +775,7 @@
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn unelevated color="primary" type="submit">Send</q-btn>
<q-btn
:label="$t('cancel')"
v-close-popup
@@ -694,7 +785,6 @@
></q-btn>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-form
@@ -708,6 +798,7 @@
v-model.trim="parse.data.request"
type="textarea"
:label="$t('paste_invoice_label')"
ref="textArea"
>
</q-input>
<div class="row q-mt-lg">
@@ -718,6 +809,17 @@
type="submit"
:label="$t('read')"
></q-btn>
<q-icon
name="content_paste"
color="grey"
class="q-mt-xs q-ml-sm q-mr-auto"
v-if="parse.copy.show"
@click="pasteToTextArea"
>
<q-tooltip>
<span v-text="$t('paste_from_clipboard')"></span>
</q-tooltip>
</q-icon>
<q-btn
v-close-popup
flat
@@ -750,7 +852,7 @@
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-dialog v-model="parse.camera.show" position="top">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@@ -771,42 +873,67 @@
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show">
<q-dialog v-model="paymentsChart.show" position="top">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<div class="row q-gutter-sm justify-between">
<div class="text-h6">Payments Chart</div>
<q-select
label="Group"
filled
dense
v-model="paymentsChart.group"
style="min-width: 120px"
:options="paymentsChart.groupOptions"
>
</q-select>
</div>
<canvas ref="canvas" width="600" height="400"></canvas>
</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"
icon="file_download"
@click="showReceiveDialog"
:label="$t('receive')"
>
</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="$t('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-dialog v-model="disclaimerDialog.show" position="top">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
<p v-text="$t('disclaimer_dialog')"></p>
<h6
class="q-my-md text-primary"
v-text="$t('disclaimer_dialog_title')"
></h6>
<p class="whitespace-pre-line" v-text="$t('disclaimer_dialog')"></p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
:label="$t('copy_wallet_url')"
type="a"
href="/account"
:label="$t('my_account')"
></q-btn>
<q-btn
v-close-popup
@@ -0,0 +1,308 @@
<q-tab-panel name="channels">
<q-dialog v-model="connectPeerDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="connectPeerDialog.data.uri"
label="Node URI"
hint="pubkey@host:port"
></q-input>
<div class="row q-mt-lg">
<q-btn
:label="$t('connect')"
color="primary"
@click="connectPeer"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="openChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<q-input
dense
type="text"
filled
v-model="openChannelDialog.data.peer_id"
label="Peer ID"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.funding_amount"
label="Funding Amount"
></q-input>
<q-expansion-item icon="warning" label="Advanced">
<q-card>
<q-card-section>
<div class="column q-gutter-md">
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.push_amount"
label="Push Amount"
hint="This gifts sats to the other side!"
></q-input>
<q-input
dense
type="number"
filled
v-model.number="openChannelDialog.data.fee_rate"
label="Fee Rate"
></q-input>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<div class="row q-mt-lg">
<q-btn
:label="$t('open')"
color="primary"
@click="openChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="closeChannelDialog.show">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<div>
<q-checkbox v-model="closeChannelDialog.data.force" label="Force" />
</div>
<div class="row q-mt-lg">
<q-btn
:label="$t('close')"
color="primary"
@click="closeChannel"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-card-section class="q-pa-none">
<div class="row q-col-gutter-lg">
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="q-gutter-y-sm">
<div class="row items-center q-mt-none q-gutter-x-sm no-wrap">
<div class="col-grow text-h6 q-my-none col-grow">Channels</div>
<q-input
filled
dense
clearable
v-model="channels.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-select
dense
size="sm"
style="min-width: 200px"
filled
multiple
clearable
v-model="stateFilters"
:options="this.states"
class="col-auto"
></q-select>
<q-btn
unelevated
color="primary"
size="md"
class="col-auto"
@click="showOpenChannelDialog()"
>
Open channel
</q-btn>
</div>
<div>
<div class="text-subtitle1 col-grow">Total</div>
<lnbits-channel-balance
:balance="this.totalBalance"
></lnbits-channel-balance>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:data="this.filteredChannels"
:filter="channels.filter"
no-data-label="No channels opened"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="q-pb-sm">
<div class="row items-center q-gutter-sm">
<div
class="text-subtitle1 col-grow"
v-text="props.row.name"
></div>
<div class="text-caption" v-if="props.row.short_id">
<span v-text="props.row.short_id"></span>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.short_id)"
></q-btn>
</div>
<q-badge
rounded
:color="states.find(s => s.value == props.row.state)?.color"
v-text="states.find(s => s.value == props.row.state)?.label"
>
</q-badge>
<q-btn
:disable='props.row.state !== "active"'
flat
dense
size="md"
@click="showCloseChannelDialog(props.row)"
icon="cancel"
color="pink"
></q-btn>
</div>
<lnbits-channel-balance
:balance="props.row.balance"
:color="props.row.color"
></lnbits-channel-balance>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-xl-6">
<q-card class="full-height">
<q-card-section class="column q-gutter-y-sm">
<div
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
>
<div class="col-grow text-h6 q-my-none">Peers</div>
<q-input
filled
dense
clearable
v-model="peers.filter"
placeholder="Search..."
class="col-auto"
></q-input>
<q-btn
class="col-auto"
color="primary"
@click="connectPeerDialog.show = true"
>
Connect Peer
</q-btn>
</div>
<q-separator></q-separator>
<q-table
dense
flat
:data="peers.data"
:filter="peers.filter"
no-data-label="No transactions made yet"
>
<template v-slot:header="props">
<q-tr :props="props" style="height: 0"> </q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<div class="row no-wrap items-center q-gutter-sm">
<div class="q-my-sm col-grow">
<div
class="text-subtitle1 text-bold"
v-text="props.row.alias"
></div>
<div class="row items-center q-gutter-sm">
<q-badge
:style="`background-color: #${props.row.color}`"
class="text-bold"
v-text="'#'+props.row.color"
>
</q-badge>
<div
class="text-bold"
v-text="shortenNodeId(props.row.id)"
></div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(props.row.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="showNodeInfoDialog(props.row)"
></q-btn>
</div>
</div>
<q-btn
unelevated
color="primary"
@click="showOpenChannelDialog(props.row.id)"
>
Open channel
</q-btn>
<q-btn
flat
dense
size="md"
@click="disconnectPeer(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</div>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,68 @@
<q-tab-panel name="dashboard">
<q-card-section class="q-pa-none">
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat title="Balance" :msat="this.info.balance_msat" />
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Fees collected"
:msat="this.info.fees?.total_msat"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Balance"
:btc="this.info.onchain_balance_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
title="Onchain Confirmed"
:btc="this.info.onchain_confirmed_sat / 100000000"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats
:stats="this.channel_stats"
></lnbits-channel-stats>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,310 @@
<q-tab-panel name="transactions">
<q-card-section class="q-pa-none">
<q-dialog v-model="transactionDetailsDialog.show">
<q-card class="my-card">
<q-card-section>
<div class="text-center q-mb-lg">
<div
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
>
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span v-text="$t('payment_received')"></span>
</div>
<div class="row q-my-md">
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
<div class="col-9 text-wrap mono">
<span
v-text="transactionDetailsDialog.data.payment_hash"
></span>
<q-icon
name="content_copy"
@click="copyText(transactionDetailsDialog.data.payment_hash)"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
<div
class="row"
v-if="transactionDetailsDialog.data.preimage && !transactionDetailsDialog.data.pending"
>
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
<div class="col-9 text-wrap mono">
<span v-text="transactionDetailsDialog.data.preimage"></span>
<q-icon
name="content_copy"
@click="copyText(transactionDetailsDialog.data.preimage)"
size="1em"
color="grey"
class="q-mb-xs cursor-pointer"
/>
</div>
</div>
</div>
<div
v-if="transactionDetailsDialog.data.bolt11"
class="text-center q-mb-lg"
>
<a :href="'lightning:' + transactionDetailsDialog.data.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="'lightning:' + transactionDetailsDialog.data.bolt11.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
<q-btn
outline
color="grey"
@click="copyText(transactionDetailsDialog.data.bolt11)"
:label="$t('copy_invoice')"
class="q-mt-sm"
></q-btn>
</div>
</div>
</q-card-section>
</q-card>
</q-dialog>
<div class="row q-col-gutter-md q-pb-lg"></div>
<div class="row q-col-gutter-lg">
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Payments</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:data="paymentsTable.data"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
row-key="payment_hash"
no-data-label="No transactions made yet"
:filter="paymentsTable.filter"
@request="getPayments"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_made"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-dialog v-model="props.row.expand" :props="props">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<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>
<span v-text="$t('invoice_waiting')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
<div
v-if="props.row.bolt11"
class="text-center q-mb-lg"
>
<a :href="'lightning:' + props.row.bolt11">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="'lightning:' + props.row.bolt11.toUpperCase()"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(props.row.bolt11)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
<q-icon
size="18px"
:name="'call_received'"
:color="'green'"
></q-icon>
<span v-text="$t('payment_received')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isPaid && props.row.isOut">
<q-icon
size="18px"
:name="'call_made'"
:color="'pink'"
></q-icon>
<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>
<span v-text="$t('outgoing_payment_pending')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
</div>
</q-card>
</q-dialog>
</q-td>
</template>
<template v-slot:body-cell-date="props">
<q-td auto-width key="date" :props="props">
<lnbits-date :ts="props.row.time"></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-destination="props">
<q-td auto-width key="destination">
<div class="row items-center justify-between no-wrap">
<q-badge
:style="`background-color: #${props.row.destination?.color}`"
class="text-bold"
v-text="props.row.destination?.alias"
></q-badge>
<div>
<q-btn
size="xs"
flat
dense
icon="content_paste"
@click="copyText(info.id)"
></q-btn>
<q-btn
size="xs"
flat
dense
icon="qr_code"
@click="showNodeInfoDialog(props.row.destination)"
></q-btn>
</div>
</div>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-lg-6 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col text-h6 q-my-none">Invoices</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
class="q-mb-md"
>
</q-input>
</div>
<q-table
dense
flat
:data="invoiceTable.data"
:columns="invoiceTable.columns"
:pagination.sync="invoiceTable.pagination"
no-data-label="No transactions made yet"
:filter="invoiceTable.filter"
@request="getInvoices"
>
<template v-slot:body-cell-pending="props">
<q-td auto-width class="text-center">
<q-icon
v-if="!props.row.pending"
size="xs"
name="call_received"
color="green"
@click="showTransactionDetailsDialog(props.row)"
></q-icon>
<q-icon
v-else
size="xs"
name="settings_ethernet"
color="grey"
@click="showTransactionDetailsDialog(props.row)"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
</q-td>
</template>
<template v-slot:body-cell-paid_at="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.paid_at"
:ts="props.row.paid_at"
></lnbits-date>
</q-td>
</template>
<template v-slot:body-cell-expiry="props">
<q-td auto-width :props="props">
<lnbits-date
v-if="props.row.expiry"
:ts="props.row.expiry"
></lnbits-date>
</q-td>
</template>
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
+376
View File
@@ -0,0 +1,376 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<q-dialog v-model="nodeInfoDialog.show">
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
</q-dialog>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<div class="q-pa-md">
<div class="q-gutter-y-md">
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="dashboard"
:label="$t('dashboard')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="channels"
:label="$t('channels')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="transactions"
:label="$t('transactions')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
<q-form name="settings_form" id="settings_form">
<q-tab-panels v-model="tab" animated>
{% include "node/_tab_dashboard.html" %} {% include
"node/_tab_channels.html" %} {% include "node/_tab_transactions.html"
%}
</q-tab-panels>
</q-form>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
new Vue({
el: '#vue',
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
mixins: [windowMixin],
data: function () {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: {
data: [],
filter: ''
},
activeBalance: {},
ranks: {},
peers: {
data: [],
filter: ''
},
connectPeerDialog: {
show: false,
data: {}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
transactionDetailsDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
],
stateFilters: [
{label: 'Active', value: 'active'},
{label: 'Pending', value: 'pending'}
],
paymentsTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'date',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
},
{
name: 'fee',
align: 'right',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
},
{
name: 'destination',
align: 'right',
label: 'Destination',
field: 'destination'
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
},
invoiceTable: {
data: [],
columns: [
{
name: 'pending',
label: ''
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'paid_at',
field: 'paid_at',
align: 'right',
label: 'Paid at',
sortable: true
},
{
name: 'expiry',
label: this.$t('expiry'),
field: 'expiry',
align: 'right',
sortable: true
},
{
name: 'amount',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: row => this.formatMsat(row.amount),
sortable: true
}
],
pagination: {
rowsPerPage: 10,
page: 1,
rowsNumber: 10
},
filter: null
}
}
},
created: function () {
this.getInfo()
this.get1MLStats()
},
watch: {
tab: function (val) {
if (val === 'transactions' && !this.paymentsTable.data.length) {
this.getPayments()
this.getInvoices()
} else if (val === 'channels' && !this.channels.data.length) {
this.getChannels()
this.getPeers()
}
}
},
computed: {
checkChanges() {
return !_.isEqual(this.settings, this.formData)
},
filteredChannels: function () {
return this.stateFilters
? this.channels.data.filter(channel => {
return this.stateFilters.find(({value}) => value == channel.state)
})
: this.channels.data
},
totalBalance: function () {
return this.filteredChannels.reduce(
(balance, channel) => {
balance.local_msat += channel.balance.local_msat
balance.remote_msat += channel.balance.remote_msat
balance.total_msat += channel.balance.total_msat
return balance
},
{local_msat: 0, remote_msat: 0, total_msat: 0}
)
}
},
methods: {
formatMsat: function (msat) {
return LNbits.utils.formatMsat(msat)
},
api: function (method, url, options) {
const params = new URLSearchParams(options?.query)
return LNbits.api
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
getChannels: function () {
return this.api('GET', '/channels').then(response => {
this.channels.data = response.data
})
},
getInfo: function () {
return this.api('GET', '/info').then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats: function () {
return this.api('GET', '/rank').then(response => {
this.ranks = response.data
})
},
getPayments: function (props) {
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/payments', {query}).then(response => {
this.paymentsTable.data = response.data.data
this.paymentsTable.pagination.rowsNumber = response.data.total
})
},
getInvoices: function (props) {
if (props) {
this.invoiceTable.pagination = props.pagination
}
let pagination = this.invoiceTable.pagination
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage ?? 0
}
return this.api('GET', '/invoices', {query}).then(response => {
this.invoiceTable.data = response.data.data
this.invoiceTable.pagination.rowsNumber = response.data.total
})
},
getPeers: function () {
return this.api('GET', '/peers').then(response => {
this.peers.data = response.data
console.log('peers', this.peers)
})
},
connectPeer: function () {
console.log('peer', this.connectPeerDialog)
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
() => {
this.connectPeerDialog.show = false
this.getPeers()
}
)
},
disconnectPeer: function (id) {
LNbits.utils
.confirmDialog('Do you really wanna disconnect this peer?')
.onOk(() => {
this.api('DELETE', `/peers/${id}`).then(response => {
this.$q.notify({
message: 'Disconnected',
icon: null
})
this.needsRestart = true
this.getPeers()
})
})
},
openChannel: function () {
this.api('POST', '/channels', {data: this.openChannelDialog.data})
.then(response => {
this.openChannelDialog.show = false
this.getChannels()
})
.catch(error => {
console.log(error)
})
},
showCloseChannelDialog: function (channel) {
this.closeChannelDialog.show = true
this.closeChannelDialog.data = {
force: false,
short_id: channel.short_id,
...channel.point
}
},
closeChannel: function () {
this.api('DELETE', '/channels', {
query: this.closeChannelDialog.data
}).then(response => {
this.closeChannelDialog.show = false
this.getChannels()
})
},
showOpenChannelDialog: function (peer_id) {
this.openChannelDialog.show = true
this.openChannelDialog.data = {peer_id, funding_amount: 0}
},
showNodeInfoDialog: function (node) {
this.nodeInfoDialog.show = true
this.nodeInfoDialog.data = node
},
showTransactionDetailsDialog: function (details) {
this.transactionDetailsDialog.show = true
this.transactionDetailsDialog.data = details
console.log('details', details)
},
exportCSV: function () {},
shortenNodeId
}
})
</script>
{% endblock %}
+136
View File
@@ -0,0 +1,136 @@
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
context %} {% block page %}
<div class="q-ma-lg-xl q-mx-auto q-ma-xl" style="max-width: 1048px">
<lnbits-node-info :info="this.info"></lnbits-node-info>
<div class="row q-col-gutter-lg q-mt-sm">
<div class="col-12 col-md-8 q-gutter-y-md">
<div class="row q-col-gutter-md q-pb-lg">
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('total_capacity')"
:msat="this.channel_stats.total_capacity"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('avg_channel_size')"
:msat="this.channel_stats.avg_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('biggest_channel_size')"
:msat="this.channel_stats.biggest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
<div class="col-12 col-md-6 q-gutter-y-md">
<lnbits-stat
:title="$t('smallest_channel_size')"
:msat="this.channel_stats.smallest_size"
/>
</div>
</div>
</div>
<div class="column col-12 col-md-4 q-gutter-y-md">
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
<lnbits-channel-stats :stats="this.channel_stats"></lnbits-channel-stats>
</div>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
new Vue({
el: '#vue',
config: {
globalProperties: {
LNbits,
msg: 'hello'
}
},
mixins: [windowMixin],
data: function () {
return {
isSuperUser: false,
wallet: {},
tab: 'dashboard',
payments: 1000,
info: {},
channel_stats: {},
channels: [],
activeBalance: {},
ranks: {},
peers: [],
connectPeerDialog: {
show: false,
data: {}
},
openChannelDialog: {
show: false,
data: {}
},
closeChannelDialog: {
show: false,
data: {}
},
nodeInfoDialog: {
show: false,
data: {}
},
states: [
{label: 'Active', value: 'active', color: 'green'},
{label: 'Pending', value: 'pending', color: 'orange'},
{label: 'Inactive', value: 'inactive', color: 'grey'},
{label: 'Closed', value: 'closed', color: 'red'}
]
}
},
created: function () {
this.getInfo()
this.get1MLStats()
},
methods: {
formatMsat: function (msat) {
return LNbits.utils.formatMsat(msat)
},
api: function (method, url, data) {
return LNbits.api.request(method, '/node/public/api/v1' + url, {}, data)
},
getInfo: function () {
this.api('GET', '/info', {}).then(response => {
this.info = response.data
this.channel_stats = response.data.channel_stats
})
},
get1MLStats: function () {
this.api('GET', '/rank', {}).then(response => {
this.ranks = response.data
})
}
}
})
</script>
{% endblock %}
@@ -1,6 +1,6 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
// update cache version every time there is a new deployment
// so the service worker reinitializes the cache
const CURRENT_CACHE = 'lnbits-{{ cache_version }}-'
const getApiKey = request => {
let api_key = request.headers.get('X-Api-Key')
@@ -16,8 +16,7 @@ self.addEventListener('activate', evt =>
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
if (currentCacheVersion !== CACHE_VERSION) {
if (!cacheName.startsWith(CURRENT_CACHE)) {
return caches.delete(cacheName)
}
})
@@ -55,3 +54,32 @@ self.addEventListener('fetch', event => {
)
}
})
// Handle and show incoming push notifications
self.addEventListener('push', function (event) {
if (!(self.Notification && self.Notification.permission === 'granted')) {
return
}
let data = event.data.json()
const title = data.title
const body = data.body
const url = data.url
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: '/favicon.ico',
data: {
url: url
}
})
)
})
// User can click on the notification message to open wallet
// Installed app will open when `url_handlers` in web app manifest is supported
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(clients.openWindow(event.notification.data.url))
})
+71 -23
View File
@@ -6,22 +6,64 @@ from subprocess import Popen
from typing import Optional
from urllib.parse import urlparse
from fastapi import Body, Depends
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet
from lnbits.core.models import User
from lnbits.core.services import update_cached_settings, update_wallet_balance
from lnbits.core.models import CreateTopup, User
from lnbits.core.services import (
get_balance_delta,
update_cached_settings,
update_wallet_balance,
)
from lnbits.core.tasks import api_invoice_listeners
from lnbits.decorators import check_admin, check_super_user
from lnbits.server import server_restart
from lnbits.settings import AdminSettings, EditableSettings, settings
from lnbits.settings import AdminSettings, UpdateSettings, settings
from lnbits.tasks import invoice_listeners
from .. import core_app
from .. import core_app_extra
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
admin_router = APIRouter()
@core_app.get("/admin/api/v1/settings/")
@admin_router.get(
"/admin/api/v1/audit",
name="Audit",
description="show the current balance of the node and the LNbits database",
dependencies=[Depends(check_admin)],
)
async def api_auditor():
try:
delta, node_balance, total_balance = await get_balance_delta()
return {
"delta_msats": int(delta),
"node_balance_msats": int(node_balance),
"lnbits_balance_msats": int(total_balance),
}
except Exception:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Could not audit balance.",
)
@admin_router.get(
"/admin/api/v1/monitor",
name="Monitor",
description="show the current listeners and other monitoring data",
dependencies=[Depends(check_admin)],
)
async def api_monitor():
return {
"invoice_listeners": list(invoice_listeners.keys()),
"api_invoice_listeners": list(api_invoice_listeners.keys()),
}
@admin_router.get("/admin/api/v1/settings/", response_model=Optional[AdminSettings])
async def api_get_settings(
user: User = Depends(check_admin),
) -> Optional[AdminSettings]:
@@ -29,18 +71,20 @@ async def api_get_settings(
return admin_settings
@core_app.put(
@admin_router.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: UpdateSettings, 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())
core_app_extra.register_new_ratelimiter()
return {"status": "Success"}
@core_app.delete(
@admin_router.delete(
"/admin/api/v1/settings/",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
@@ -50,7 +94,7 @@ async def api_delete_settings() -> None:
server_restart.set()
@core_app.get(
@admin_router.get(
"/admin/api/v1/restart/",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
@@ -60,27 +104,31 @@ async def api_restart_server() -> dict[str, str]:
return {"status": "Success"}
@core_app.put(
@admin_router.put(
"/admin/api/v1/topup/",
name="Topup",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
)
async def api_topup_balance(
id: str = Body(...), amount: int = Body(...)
) -> dict[str, str]:
async def api_topup_balance(data: CreateTopup) -> dict[str, str]:
try:
await get_wallet(id)
except:
await get_wallet(data.id)
except Exception:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="wallet does not exist."
)
await update_wallet_balance(wallet_id=id, amount=int(amount))
if settings.lnbits_backend_wallet_class == "VoidWallet":
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="VoidWallet active"
)
await update_wallet_balance(wallet_id=data.id, amount=int(data.amount))
return {"status": "Success"}
@core_app.get(
@admin_router.get(
"/admin/api/v1/backup/",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
@@ -97,10 +145,10 @@ async def api_download_backup() -> FileResponse:
p = urlparse(db_url)
command = (
f"pg_dump --host={p.hostname} "
f'--dbname={p.path.replace("/", "")} '
f"--dbname={p.path.replace('/', '')} "
f"--username={p.username} "
f"--no-password "
f"--format=c "
"--no-password "
"--format=c "
f"--file={pg_backup_filename}"
)
proc = Popen(
+380 -281
View File
File diff suppressed because it is too large Load Diff
+346
View File
@@ -0,0 +1,346 @@
import importlib
from typing import Callable, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_sso.sso.base import OpenID, SSOBase
from loguru import logger
from starlette.status import (
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_500_INTERNAL_SERVER_ERROR,
)
from lnbits.decorators import check_user_exists
from lnbits.helpers import (
create_access_token,
decrypt_internal_message,
encrypt_internal_message,
is_valid_email_address,
is_valid_username,
)
from lnbits.settings import AuthMethods, settings
from ..crud import (
create_account,
create_user,
get_account,
get_account_by_email,
get_account_by_username_or_email,
get_user,
update_account,
update_user_password,
verify_user_password,
)
from ..models import (
CreateUser,
LoginUsernamePassword,
LoginUsr,
UpdateSuperuserPassword,
UpdateUser,
UpdateUserPassword,
User,
UserConfig,
)
auth_router = APIRouter()
@auth_router.get("/api/v1/auth", description="Get the authenticated user")
async def get_auth_user(user: User = Depends(check_user_exists)) -> User:
return user
@auth_router.post("/api/v1/auth", description="Login via the username and password")
async def login(data: LoginUsernamePassword) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTP_401_UNAUTHORIZED, "Login by 'Username and Password' not allowed."
)
try:
user = await get_account_by_username_or_email(data.username)
if not user:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.")
if not await verify_user_password(user.id, data.password):
raise HTTPException(HTTP_401_UNAUTHORIZED, "Invalid credentials.")
return _auth_success_response(user.username, user.id)
except HTTPException as e:
raise e
except Exception as e:
logger.debug(e)
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
@auth_router.post("/api/v1/auth/usr", description="Login via the User ID")
async def login_usr(data: LoginUsr) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.user_id_only):
raise HTTPException(HTTP_401_UNAUTHORIZED, "Login by 'User ID' not allowed.")
try:
user = await get_user(data.usr)
if not user:
raise HTTPException(HTTP_401_UNAUTHORIZED, "User ID does not exist.")
return _auth_success_response(user.username or "", user.id)
except HTTPException as e:
raise e
except Exception as e:
logger.debug(e)
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot login.")
@auth_router.get("/api/v1/auth/{provider}", description="SSO Provider")
async def login_with_sso_provider(
request: Request, provider: str, user_id: Optional[str] = None
):
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
)
provider_sso.redirect_uri = str(request.base_url) + f"api/v1/auth/{provider}/token"
with provider_sso:
state = encrypt_internal_message(user_id)
return await provider_sso.get_login_redirect(state=state)
@auth_router.get("/api/v1/auth/{provider}/token", description="Handle OAuth callback")
async def handle_oauth_token(request: Request, provider: str) -> RedirectResponse:
provider_sso = _new_sso(provider)
if not provider_sso:
raise HTTPException(
HTTP_401_UNAUTHORIZED, f"Login by '{provider}' not allowed."
)
try:
with provider_sso:
userinfo = await provider_sso.verify_and_process(request)
assert userinfo is not None
user_id = decrypt_internal_message(provider_sso.state)
request.session.pop("user", None)
return await _handle_sso_login(userinfo, user_id)
except HTTPException as e:
raise e
except ValueError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR,
f"Cannot authenticate user with {provider} Auth.",
)
@auth_router.post("/api/v1/auth/logout")
async def logout() -> JSONResponse:
response = JSONResponse({"status": "success"}, status_code=status.HTTP_200_OK)
response.delete_cookie("cookie_access_token")
response.delete_cookie("is_lnbits_user_authorized")
response.delete_cookie("is_access_token_expired")
response.delete_cookie("lnbits_last_active_wallet")
return response
@auth_router.post("/api/v1/auth/register")
async def register(data: CreateUser) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTP_401_UNAUTHORIZED, "Register by 'Username and Password' not allowed."
)
if data.password != data.password_repeat:
raise HTTPException(HTTP_400_BAD_REQUEST, "Passwords do not match.")
if not data.username:
raise HTTPException(HTTP_400_BAD_REQUEST, "Missing username.")
if not is_valid_username(data.username):
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.")
if data.email and not is_valid_email_address(data.email):
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
try:
user = await create_user(data)
return _auth_success_response(user.username)
except ValueError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot create user.")
@auth_router.put("/api/v1/auth/password")
async def update_password(
data: UpdateUserPassword, user: User = Depends(check_user_exists)
) -> Optional[User]:
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
raise HTTPException(
HTTP_401_UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
)
if data.user_id != user.id:
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.")
try:
return await update_user_password(data)
except AssertionError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
)
@auth_router.put("/api/v1/auth/update")
async def update(
data: UpdateUser, user: User = Depends(check_user_exists)
) -> Optional[User]:
if data.user_id != user.id:
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid user ID.")
if data.username and not is_valid_username(data.username):
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid username.")
if data.email != user.email:
raise HTTPException(HTTP_400_BAD_REQUEST, "Email mismatch.")
try:
return await update_account(user.id, data.username, None, data.config)
except AssertionError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException(HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user.")
@auth_router.put("/api/v1/auth/first_install")
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install:
raise HTTPException(HTTP_401_UNAUTHORIZED, "This is not your first install")
try:
await update_account(
user_id=settings.super_user,
username=data.username,
user_config=UserConfig(provider="lnbits"),
)
super_user = UpdateUserPassword(
user_id=settings.super_user,
password=data.password,
password_repeat=data.password_repeat,
username=data.username,
)
await update_user_password(super_user)
settings.first_install = False
return _auth_success_response(username=super_user.username)
except AssertionError as e:
raise HTTPException(HTTP_403_FORBIDDEN, str(e))
except Exception as e:
logger.debug(e)
raise HTTPException(
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
)
async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] = None):
email = userinfo.email
if not email or not is_valid_email_address(email):
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet"
user_config = UserConfig(**dict(userinfo))
user_config.email_verified = True
account = await get_account_by_email(email)
if verified_user_id:
if account:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Email already used.")
account = await get_account(verified_user_id)
if not account:
raise HTTPException(HTTP_401_UNAUTHORIZED, "Cannot verify user email.")
redirect_path = "/account"
if account:
user = await update_account(account.id, email=email, user_config=user_config)
else:
if not settings.new_accounts_allowed:
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
user = await create_account(email=email, user_config=user_config)
if not user:
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
return _auth_redirect_response(redirect_path, email)
def _auth_success_response(
username: Optional[str] = None,
user_id: Optional[str] = None,
email: Optional[str] = None,
) -> JSONResponse:
access_token = create_access_token(
data={"sub": username or "", "usr": user_id, "email": email}
)
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
access_token = create_access_token(data={"sub": "" or "", "email": email})
response = RedirectResponse(path)
response.set_cookie("cookie_access_token", access_token, httponly=True)
response.set_cookie("is_lnbits_user_authorized", "true")
response.delete_cookie("is_access_token_expired")
return response
def _new_sso(provider: str) -> Optional[SSOBase]:
try:
if not settings.is_auth_method_allowed(AuthMethods(f"{provider}-auth")):
return None
client_id = getattr(settings, f"{provider}_client_id", None)
client_secret = getattr(settings, f"{provider}_client_secret", None)
discovery_url = getattr(settings, f"{provider}_discovery_url", None)
if not client_id or not client_secret:
logger.warning(f"{provider} auth allowed but not configured.")
return None
SSOProviderClass = _find_auth_provider_class(provider)
ssoProvider = SSOProviderClass(
client_id, client_secret, None, allow_insecure_http=True
)
if (
discovery_url
and getattr(ssoProvider, "discovery_url", discovery_url) != discovery_url
):
ssoProvider.discovery_url = discovery_url
return ssoProvider
except Exception as e:
logger.warning(e)
return None
def _find_auth_provider_class(provider: str) -> Callable:
sso_modules = ["lnbits.core.sso", "fastapi_sso.sso"]
for module in sso_modules:
try:
provider_module = importlib.import_module(f"{module}.{provider}")
ProviderClass = getattr(provider_module, f"{provider.title()}SSO")
if ProviderClass:
return ProviderClass
except Exception:
pass
raise ValueError(f"No SSO provider found for '{provider}'.")
+265 -156
View File
@@ -1,27 +1,32 @@
import asyncio
import sys
from http import HTTPStatus
from typing import List, Optional
from pathlib import Path
from typing import Annotated, List, Optional, Union
from urllib.parse import urlparse
from fastapi import Depends, Query, Request, status
from fastapi import Cookie, Depends, Query, Request, status
from fastapi.exceptions import HTTPException
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse
from lnbits.core import db
from lnbits.core.db import core_app_extra, 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
from lnbits.settings import get_wallet_class, settings
from lnbits.settings import settings
from lnbits.wallets import get_wallet_class
from ...extension_manager import InstallableExtension, get_valid_extensions
from ...utils.exchange_rates import allowed_currencies, currencies
from ..crud import (
create_account,
create_wallet,
delete_wallet,
get_balance_check,
get_dbversions,
get_inactive_extensions,
get_installed_extensions,
get_user,
@@ -31,22 +36,40 @@ from ..crud import (
)
from ..services import pay_invoice, redeem_lnurl_withdraw
core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"])
generic_router = APIRouter(
tags=["Core NON-API Website Routes"], include_in_schema=False
)
@core_html_routes.get("/favicon.ico", response_class=FileResponse)
@generic_router.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return FileResponse("lnbits/core/static/favicon.ico")
return FileResponse(Path("lnbits", "static", "favicon.ico"))
@core_html_routes.get("/", response_class=HTMLResponse)
@generic_router.get("/", response_class=HTMLResponse)
async def home(request: Request, lightning: str = ""):
return template_renderer().TemplateResponse(
"core/index.html", {"request": request, "lnurl": lightning}
)
@core_html_routes.get("/robots.txt", response_class=HTMLResponse)
@generic_router.get("/first_install", response_class=HTMLResponse)
async def first_install(request: Request):
if not settings.first_install:
return template_renderer().TemplateResponse(
"error.html",
{
"request": request,
"err": "Super user account has already been configured.",
},
)
return template_renderer().TemplateResponse(
"core/first_install.html",
{"request": request},
)
@generic_router.get("/robots.txt", response_class=HTMLResponse)
async def robots():
data = """
User-agent: *
@@ -55,44 +78,27 @@ async def robots():
return HTMLResponse(content=data, media_type="text/plain")
@core_html_routes.get(
"/extensions", name="core.extensions", response_class=HTMLResponse
)
async def extensions(
request: Request,
user: User = Depends(check_user_exists),
enable: str = Query(None),
disable: str = Query(None),
):
await toggle_extension(enable, disable, user.id)
# 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
@generic_router.get(
"/extensions", 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),
enable: str = Query(None),
disable: str = Query(None),
):
await toggle_extension(enable, disable, user.id)
try:
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
installable_exts: List[
InstallableExtension
] = await InstallableExtension.get_installable_extensions()
installable_exts = await InstallableExtension.get_installable_extensions()
installable_exts_ids = [e.id for e in installable_exts]
installable_exts += [
e for e in installed_exts if e.id not in installed_exts_ids
e for e in installed_exts if e.id not in installable_exts_ids
]
for e in installable_exts:
@@ -110,21 +116,30 @@ async def extensions_install(
try:
ext_id = activate or deactivate
all_extensions = get_valid_extensions()
ext = next((e for e in all_extensions if e.code == ext_id), None)
if ext_id and user.admin:
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [deactivate]
elif activate:
# if extension never loaded (was deactivated on server startup)
if ext_id not in sys.modules.keys():
# run extension start-up routine
core_app_extra.register_new_ext_routes(ext)
settings.lnbits_deactivated_extensions = list(
filter(
lambda e: e != activate, settings.lnbits_deactivated_extensions
)
)
await update_installed_extension_state(
ext_id=ext_id, active=activate is not None
)
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
all_ext_ids = list(map(lambda e: e.code, all_extensions))
inactive_extensions = await get_inactive_extensions()
db_version = await get_dbversions()
extensions = list(
map(
lambda ext: {
@@ -136,21 +151,26 @@ async def extensions_install(
"isFeatured": ext.featured,
"dependencies": ext.dependencies,
"isInstalled": ext.id in installed_exts_ids,
"isAvailable": ext.id in all_extensions,
"hasDatabaseTables": ext.id in db_version,
"isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions,
"latestRelease": dict(ext.latest_release)
if ext.latest_release
else None,
"installedRelease": dict(ext.installed_release)
if ext.installed_release
else None,
"latestRelease": (
dict(ext.latest_release) if ext.latest_release else None
),
"installedRelease": (
dict(ext.installed_release) if ext.installed_release else None
),
},
installable_exts,
)
)
# refresh user state. Eg: enabled extensions.
user = await get_user(user.id) or user
return template_renderer().TemplateResponse(
"core/install.html",
"core/extensions.html",
{
"request": request,
"user": user.dict(),
@@ -162,94 +182,83 @@ async def extensions_install(
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
@core_html_routes.get(
@generic_router.get(
"/wallet",
response_class=HTMLResponse,
description="""
Args:
just **wallet_name**: create a new user, then create a new wallet for user with wallet_name<br>
just **user_id**: return the first user wallet or create one if none found (with default wallet_name)<br>
**user_id** and **wallet_name**: create a new wallet for user with wallet_name<br>
**user_id** and **wallet_id**: return that wallet if user is the owner<br>
nothing: create everything<br>
""",
description="show wallet page",
)
async def wallet(
request: Request = Query(None),
nme: Optional[str] = Query(None),
usr: Optional[UUID4] = Query(None),
request: Request,
lnbits_last_active_wallet: Annotated[Union[str, None], Cookie()] = None,
user: User = Depends(check_user_exists),
wal: Optional[UUID4] = Query(None),
):
user_id = usr.hex if usr else None
wallet_id = wal.hex if wal else None
wallet_name = nme
if not user_id:
user = await get_user((await create_account()).id)
logger.info(f"Create user {user.id}") # type: ignore
if wal:
wallet_id = wal.hex
elif len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id)
user = await get_user(user_id=user.id) or user
wallet_id = wallet.id
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
wallet_id = lnbits_last_active_wallet
else:
user = await get_user(user_id)
if not user:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User does not exist."}
)
if (
len(settings.lnbits_allowed_users) > 0
and user_id not in settings.lnbits_allowed_users
and user_id not in settings.lnbits_admin_users
and user_id != settings.super_user
):
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User not authorized."}
)
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
user.admin = True
if user_id == settings.super_user:
user.super_user = True
wallet_id = user.wallets[0].id
if not wallet_id:
if user.wallets and not wallet_name: # type: ignore
wallet = user.wallets[0] # type: ignore
else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) # type: ignore
logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}" # type: ignore
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
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
if not userwallet:
user_wallet = user.get_wallet(wallet_id)
if not user_wallet or user_wallet.deleted:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
return template_renderer().TemplateResponse(
resp = template_renderer().TemplateResponse(
"core/wallet.html",
{
"request": request,
"user": user.dict(), # type: ignore
"wallet": userwallet.dict(),
"user": user.dict(),
"wallet": user_wallet.dict(),
"currencies": allowed_currencies(),
"service_fee": settings.lnbits_service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
)
resp.set_cookie("lnbits_last_active_wallet", wallet_id)
return resp
@generic_router.get(
"/account",
response_class=HTMLResponse,
description="show account page",
)
async def account(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
"core/account.html",
{
"request": request,
"user": user.dict(),
},
)
@core_html_routes.get("/withdraw", response_class=JSONResponse)
@generic_router.get("/withdraw", response_class=JSONResponse)
async def lnurl_full_withdraw(request: Request):
user = await get_user(request.query_params.get("usr"))
usr_param = request.query_params.get("usr")
if not usr_param:
return {"status": "ERROR", "reason": "usr parameter not provided."}
user = await get_user(usr_param)
if not user:
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.query_params.get("wal"))
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
wallet = user.get_wallet(wal_param)
if not wallet:
return {"status": "ERROR", "reason": "Wallet does not exist."}
@@ -259,27 +268,39 @@ async def lnurl_full_withdraw(request: Request):
"k1": "0",
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}",
"defaultDescription": (
f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}"
),
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
}
@core_html_routes.get("/withdraw/cb", response_class=JSONResponse)
@generic_router.get("/withdraw/cb", response_class=JSONResponse)
async def lnurl_full_withdraw_callback(request: Request):
user = await get_user(request.query_params.get("usr"))
usr_param = request.query_params.get("usr")
if not usr_param:
return {"status": "ERROR", "reason": "usr parameter not provided."}
user = await get_user(usr_param)
if not user:
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.query_params.get("wal"))
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
wallet = user.get_wallet(wal_param)
if not wallet:
return {"status": "ERROR", "reason": "Wallet does not exist."}
pr = request.query_params.get("pr")
if not pr:
return {"status": "ERROR", "reason": "payment_request not provided."}
async def pay():
try:
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
except:
except Exception:
pass
asyncio.create_task(pay())
@@ -291,49 +312,35 @@ async def lnurl_full_withdraw_callback(request: Request):
return {"status": "OK"}
@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 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
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
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
return RedirectResponse(
url_for("/"), status_code=status.HTTP_307_TEMPORARY_REDIRECT
)
@core_html_routes.get("/withdraw/notify/{service}")
@generic_router.get("/withdraw/notify/{service}")
async def lnurl_balance_notify(request: Request, service: str):
bc = await get_balance_check(request.query_params.get("wal"), service)
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
bc = await get_balance_check(wal_param, service)
if bc:
await redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get(
@generic_router.get(
"/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet"
)
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)
lightning_param = request.query_params.get("lightning")
if not lightning_param:
return {"status": "ERROR", "reason": "lightning parameter not provided."}
asyncio.create_task(
redeem_lnurl_withdraw(
wallet.id,
request.query_params.get("lightning"),
lightning_param,
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service
@@ -341,18 +348,27 @@ 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,
)
@core_html_routes.get("/service-worker.js", response_class=FileResponse)
async def service_worker():
return FileResponse("lnbits/core/static/js/service-worker.js")
@generic_router.get("/service-worker.js")
async def service_worker(request: Request):
return template_renderer().TemplateResponse(
"service-worker.js",
{
"request": request,
"cache_version": settings.server_startup_time,
},
media_type="text/javascript",
)
@core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str):
@generic_router.get("/manifest/{usr}.webmanifest")
async def manifest(request: Request, usr: str):
host = urlparse(str(request.url)).netloc
user = await get_user(usr)
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
@@ -362,12 +378,49 @@ async def manifest(usr: str):
"name": settings.lnbits_site_title + " Wallet",
"icons": [
{
"src": settings.lnbits_custom_logo
if settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"src": (
settings.lnbits_custom_logo
if settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
),
"sizes": "512x512",
"type": "image/png",
"sizes": "900x900",
}
},
{"src": "/static/favicon.ico", "sizes": "32x32", "type": "image/x-icon"},
{
"src": "/static/images/maskable_icon_x192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable",
},
{
"src": "/static/images/maskable_icon_x512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable",
},
{
"src": "/static/images/maskable_icon.png",
"type": "image/png",
"sizes": "1024x1024",
"purpose": "maskable",
},
],
"screenshots": [
{
"src": "/static/images/screenshot_desktop.png",
"sizes": "2394x1314",
"type": "image/png",
"form_factor": "wide",
"label": "LNbits - Desktop screenshot",
},
{
"src": "/static/images/screenshot_phone.png",
"sizes": "1080x1739",
"type": "image/png",
"form_factor": "narrow",
"label": "LNbits - Phone screenshot",
},
],
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
"background_color": "#1F2234",
@@ -381,13 +434,59 @@ async def manifest(usr: str):
"short_name": wallet.name,
"description": wallet.name,
"url": f"/wallet?usr={usr}&wal={wallet.id}",
"icons": [
{
"src": "/static/images/maskable_icon_x96.png",
"sizes": "96x96",
"type": "image/png",
}
],
}
for wallet in user.wallets
],
"url_handlers": [{"origin": f"https://{host}"}],
}
@core_html_routes.get("/admin", response_class=HTMLResponse)
@generic_router.get("/node", response_class=HTMLResponse)
async def node(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"node/index.html",
{
"request": request,
"user": user.dict(),
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].dict(),
},
)
@generic_router.get("/node/public", response_class=HTMLResponse)
async def node_public(request: Request):
if not settings.lnbits_public_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"node/public.html",
{
"request": request,
"settings": settings.dict(),
"balance": balance,
},
)
@generic_router.get("/admin", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_admin_ui:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
@@ -402,10 +501,20 @@ async def index(request: Request, user: User = Depends(check_admin)):
"user": user.dict(),
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
},
)
@generic_router.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(
+191
View File
@@ -0,0 +1,191 @@
from http import HTTPStatus
from typing import List, Optional
import httpx
from fastapi import APIRouter, Body, Depends, HTTPException
from pydantic import BaseModel
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
from lnbits.decorators import check_admin, check_super_user, parse_filters
from lnbits.nodes import get_node_class
from lnbits.settings import settings
from ...db import Filters, Page
from ...nodes.base import (
ChannelPoint,
Node,
NodeChannel,
NodeInfoResponse,
NodeInvoice,
NodeInvoiceFilters,
NodePayment,
NodePaymentsFilters,
NodePeerInfo,
PublicNodeInfo,
)
from ...utils.cache import cache
def require_node():
NODE = get_node_class()
if not NODE:
raise HTTPException(
status_code=HTTPStatus.NOT_IMPLEMENTED,
detail="Active backend does not implement Node API",
)
if not settings.lnbits_node_ui:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail="Not enabled",
)
return NODE
def check_public():
if not (settings.lnbits_node_ui and settings.lnbits_public_node_ui):
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail="Not enabled",
)
node_router = APIRouter(prefix="/node/api/v1", dependencies=[Depends(check_admin)])
super_node_router = APIRouter(
prefix="/node/api/v1", dependencies=[Depends(check_super_user)]
)
public_node_router = APIRouter(
prefix="/node/public/api/v1", dependencies=[Depends(check_public)]
)
@node_router.get(
"/ok",
description="Check if node api can be enabled",
status_code=200,
dependencies=[Depends(require_node)],
)
async def api_get_ok():
pass
@public_node_router.get("/info", response_model=PublicNodeInfo)
async def api_get_public_info(node: Node = Depends(require_node)) -> PublicNodeInfo:
return await cache.save_result(node.get_public_info, key="node:public_info")
@node_router.get("/info")
async def api_get_info(
node: Node = Depends(require_node),
) -> Optional[NodeInfoResponse]:
return await node.get_info()
@node_router.get("/channels")
async def api_get_channels(
node: Node = Depends(require_node),
) -> Optional[List[NodeChannel]]:
return await node.get_channels()
@super_node_router.post("/channels", response_model=ChannelPoint)
async def api_create_channel(
node: Node = Depends(require_node),
peer_id: str = Body(),
funding_amount: int = Body(),
push_amount: Optional[int] = Body(None),
fee_rate: Optional[int] = Body(None),
):
return await node.open_channel(peer_id, funding_amount, push_amount, fee_rate)
@super_node_router.delete("/channels")
async def api_delete_channel(
short_id: Optional[str],
funding_txid: Optional[str],
output_index: Optional[int],
force: bool = False,
node: Node = Depends(require_node),
) -> Optional[List[NodeChannel]]:
return await node.close_channel(
short_id,
(
ChannelPoint(funding_txid=funding_txid, output_index=output_index)
if funding_txid is not None and output_index is not None
else None
),
force,
)
@node_router.get("/payments", response_model=Page[NodePayment])
async def api_get_payments(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
) -> Optional[Page[NodePayment]]:
if not settings.lnbits_node_ui_transactions:
raise HTTPException(
HTTP_503_SERVICE_UNAVAILABLE,
detail="You can enable node transactions in the Admin UI",
)
return await node.get_payments(filters)
@node_router.get("/invoices", response_model=Page[NodeInvoice])
async def api_get_invoices(
node: Node = Depends(require_node),
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
) -> Optional[Page[NodeInvoice]]:
if not settings.lnbits_node_ui_transactions:
raise HTTPException(
HTTP_503_SERVICE_UNAVAILABLE,
detail="You can enable node transactions in the Admin UI",
)
return await node.get_invoices(filters)
@node_router.get("/peers", response_model=List[NodePeerInfo])
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
return await node.get_peers()
@super_node_router.post("/peers")
async def api_connect_peer(
uri: str = Body(embed=True), node: Node = Depends(require_node)
):
return await node.connect_peer(uri)
@super_node_router.delete("/peers/{peer_id}")
async def api_disconnect_peer(peer_id: str, node: Node = Depends(require_node)):
return await node.disconnect_peer(peer_id)
class NodeRank(BaseModel):
capacity: Optional[int]
channelcount: Optional[int]
age: Optional[int]
growth: Optional[int]
availability: Optional[int]
# Same for public and private api
@node_router.get(
"/rank",
description="Retrieve node ranks from https://1ml.com",
response_model=Optional[NodeRank],
)
@public_node_router.get(
"/rank",
description="Retrieve node ranks from https://1ml.com",
response_model=Optional[NodeRank],
)
async def api_get_1ml_stats(node: Node = Depends(require_node)) -> Optional[NodeRank]:
node_id = await node.get_id()
headers = {"User-Agent": settings.user_agent}
async with httpx.AsyncClient(headers=headers) as client:
r = await client.get(url=f"https://1ml.com/node/{node_id}/json", timeout=15)
try:
r.raise_for_status()
return r.json()["noderank"]
except httpx.HTTPStatusError:
raise HTTPException(status_code=404, detail="Node not found on 1ml.com")
+6 -7
View File
@@ -1,18 +1,18 @@
import asyncio
import datetime
from http import HTTPStatus
from fastapi import HTTPException
from fastapi import APIRouter, HTTPException
from loguru import logger
from lnbits import bolt11
from .. import core_app
from ..crud import get_standalone_payment
from ..tasks import api_invoice_listeners
public_router = APIRouter()
@core_app.get("/public/v1/payment/{payment_hash}")
@public_router.get("/public/v1/payment/{payment_hash}")
async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash)
@@ -25,10 +25,9 @@ async def api_public_payment_longpolling(payment_hash):
try:
invoice = bolt11.decode(payment.bolt11)
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration < datetime.datetime.now():
if invoice.has_expired():
return {"status": "expired"}
except:
except Exception:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid bolt11 invoice."
)

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