Compare commits

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

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

* fstring

* log the error

---------

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

* readd merge conflict stuff

---------

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

* add limit and offset to filters (#1563)

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

* black

* add openapi docs

* remove example commentC

* improve typing and make nested filter possible in openapi

* typo in fn name

* readd Type

---------

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

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

* sorting

* unpack check_internal

* introduce another crud fn for checking internal paid payment

* rename

---------

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

* add node dependencies

* add bolt11-decoder

* run npm install inside dockerimage

* only use bundle.js and bundle.css

* use node_modules for bundling vendor assets

* remove dead code

* make argument optional

* reintroduce vendor dir

* reintroduce vendor and single javascript files, minification

* wrong moment, remove minification

* lock packages with non critical issues

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

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

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

* move version into settings

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

* chore: code format
2023-03-15 11:26:07 +01:00
ArcandGitHub a360a47be9 Merge pull request #1577 from lnbits/bump-version-0-10-3
bump to version 0.10.3
2023-03-10 13:03:00 +00:00
dni ⚡ 552f253b5d bump to version 0.10.3 2023-03-10 14:01:46 +01:00
ArcandGitHub 5021d86488 Merge pull request #1566 from bitkarrot/patch-3
update installation.md path
2023-03-10 13:01:22 +00:00
ArcandGitHub c257e42222 Merge pull request #1575 from wazint/ext-doc
Doc: Fix dev guide for getting started with extensions
2023-03-10 13:00:15 +00:00
ArcandGitHub 17cdf57b8f Merge pull request #1576 from prusnak/shortuuid-bump
Bump shortuuid dependency to fix build on Python 3.10
2023-03-10 12:59:04 +00:00
ArcandGitHub 375b575eed Merge pull request #1572 from lnbits/readme-wiki-link
fix markdown links in the readme
2023-03-10 12:58:28 +00:00
Pavol Rusnak f860c170fc Bump shortuuid dependency to fix build on Python 3.10 2023-03-10 11:54:23 +01:00
ArcandGitHub 095206e2ed Merge pull request #1574 from lnbits/fix-flake
fix flake error: #1088
2023-03-10 10:54:18 +00:00
ArcandGitHub 4f3159339a Merge pull request #1573 from lnbits/feat-backup-pg_dump
BUG: Dockerfile does not support backup funcionality
2023-03-10 09:58:53 +00:00
wazint 9ba2983572 Merge branch 'ext-doc' of github-tor:wazint/lnbits into ext-doc 2023-03-09 21:55:26 +02:00
wazint acaf4849d2 doc: Fix dev guide for getting started with extensions 2023-03-09 21:53:11 +02:00
dni ⚡ 3fa4135a12 non interactive install 2023-03-09 16:01:52 +01:00
dni ⚡ a8f534cd2e postgres-client version 14... 2023-03-09 15:10:50 +01:00
dni ⚡ c58fac39cd fix flake error: #1088 2023-03-09 14:50:11 +01:00
dni ⚡ 161cb9cfbc remove build not needed, create the right datadir, add postgres-client for pg_dump backup functionality 2023-03-09 13:48:44 +01:00
Pavol Rusnak 892d8ffa62 fix markdown links in the readme 2023-03-08 18:15:58 +01:00
dni ⚡andGitHub f2849a6e68 update version (#1570) 2023-03-08 13:55:58 +01:00
BitkarrotandGitHub 3d55d98d33 update installation.md
lnbits-legend -> lnbits for systemd
2023-03-07 20:14:15 -08:00
ArcandGitHub dc2a7898f7 Merge pull request #1564 from jackstar12/remove-disabled-extensions
remove lnbits_disabled_extensions
2023-03-07 13:20:56 +00:00
ArcandGitHub 2787d5c6b6 Merge pull request #1542 from lnbits/pullntxbot
RIP Lntxbot, thank you for all your help building LN, you will be missed :(
2023-03-07 12:48:44 +00:00
ArcandGitHub 39c1dcb942 Merge pull request #1546 from arbadacarbaYK/main
Readme - updated docu link
2023-03-07 12:47:08 +00:00
ArcandGitHub be078a8c52 Merge pull request #1556 from wazint/main
Doc: fix links, spelling and minor updates
2023-03-07 12:39:21 +00:00
ArcandGitHub 934fc0abbb Merge pull request #1560 from lnbits/feat-db-export
FEAT: add db export admin route for pg and sqlite, add to adminui
2023-03-07 12:04:20 +00:00
ArcandGitHub 81986c9b4d Merge pull request #1554 from lnbits/fix-baseurl
FIX: default baseurl should include port
2023-03-07 12:00:19 +00:00
ArcandGitHub 00b3ab94ba Merge pull request #1559 from lnbits/feat-fundingfallback
FEAT: add funding source fallback and VoidWallet warning for frontend
2023-03-07 11:56:01 +00:00
dni ⚡ 194b063311 fix prettier 2023-03-07 12:40:19 +01:00
dni ⚡ 235513f2ef fix if for voidwallet warning 2023-03-07 12:28:52 +01:00
jacksn 0ca4b8dea1 remove lnbits_disabled_extensions from .env.example and admin ui 2023-03-05 21:44:25 +01:00
jacksn 83f04534e7 remove lnbits_disabled_extensions, fix is_admin_only 2023-03-05 13:36:36 +01:00
dni ⚡ 6cf1d97d5a FEAT: add db export admin route for pg and sqlite, add to adminui 2023-03-02 15:22:39 +01:00
dni ⚡ 8410776ffb only make it fallback when adminui is enabled 2023-03-02 12:07:20 +01:00
dni ⚡ e93fbd24be FEAT: add funding source fallback and VoidWallet warning for frontend 2023-03-02 12:03:58 +01:00
wazint 1e39e7dd63 doc: Fix dev guide for getting started with extensions 2023-03-01 23:06:44 +02:00
wazint aac392a053 doc: fix links, spelling and minor updates 2023-03-01 21:39:40 +02:00
ArcandGitHub c81d3e06c8 Merge pull request #1555 from lnbits/fix-version
fix version to 0.10.0
2023-03-01 16:43:56 +00:00
arbadacarbaandGitHub a2ce8fc955 Merge branch 'lnbits:main' into main 2023-03-01 17:35:37 +01:00
dni ⚡ a5edcc94ff fix version to 0.10.0 2023-03-01 16:48:19 +01:00
dni ⚡ 061121d635 FIX: default baseurl should include port 2023-03-01 09:55:29 +01:00
dni ⚡andGitHub 6f194fe2f8 introduce baseurl and remove force_https (#1549)
* introduce baseurl and remove force_https

* calle's suggestions
2023-02-28 13:45:43 +01:00
61d809ebdb Update README.md
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-02-24 14:48:50 +01:00
arbadacarbaandGitHub 4df74c6896 Update README.md
swichted docs.lnbits.org - links to new wiki
2023-02-23 23:04:30 +01:00
dni ⚡andGitHub f943ca7d51 remove unused variable for test in makefile (#1540) 2023-02-23 18:15:38 +01:00
Vlad StanandGitHub f9695c0569 fix: init filteredExtensions with [] (#1543) 2023-02-23 16:32:40 +01:00
Ben Arc de445aa593 LNtxbot mentioned in a few more places 2023-02-23 11:46:29 +00:00
Ben Arc cbde5744aa RIP Lntxbot, thank you for all your help building LN, you will be missed :( 2023-02-23 11:41:44 +00:00
dni ⚡andGitHub a154ff3175 fix issue #1506 (#1541) 2023-02-23 11:40:24 +01:00
Vlad StanandGitHub 12014ee3e3 fix: hide Admin UI if LNBITS_ADMIN_UI is false (#1537)
* fix: hide Admin config if `LNBITS_ADMIN_UI` is `false`

* chore: code format

* chore: code format
2023-02-22 13:30:57 +01:00
calleandGitHub f7e43cdd95 check for amountless invoices deeper in the call stack (#1536) 2023-02-22 13:30:45 +01:00
Vlad StanandGitHub 0d5fef1cb9 Add redirect paths for extensions (#1532)
* feat: basic redirect to extension endpoint

* feat: filter by headers

* refactor: extract `middleware.py`

* fix: do not add twice the same extension to redirects

* chore: code clean-up
2023-02-22 10:12:16 +01:00
calleandGitHub 84e369aad2 UI: rename extensions button (#1535)
* UI: rename extensions button

* change JS
2023-02-22 10:01:52 +01:00
Vlad StanandGitHub 16a997663d fix: activate default installed extensions (#1533) 2023-02-22 09:43:17 +01:00
Vlad Stan 1cd5c26392 doc: add dependencies field 2023-02-22 10:15:02 +02:00
calleandGitHub c5e2bbc1c6 Installer: log process (#1530) 2023-02-21 12:19:50 +01:00
ArcandGitHub 76ffb8a607 Merge pull request #1529 from lnbits/cybertheme
Added cyber theme
2023-02-21 00:48:16 +00:00
Ben Arc 910727a7f4 Sadly had to tone down
due to font color of buttons
2023-02-20 23:17:40 +00:00
Ben Arc bf097a3493 format 2023-02-20 22:45:52 +00:00
Ben Arc 9f049c1952 Added cyber theme
Because we all liked the theme from this pr https://github.com/lnbits/lnbits/pulls?page=2&q=is%3Apr+is%3Aopen
2023-02-20 22:41:11 +00:00
ArcandGitHub 51d71afe85 Merge pull request #1528 from lnbits/extensionstweak
Users need to be able to see "add extensions" to install them, and bug their admin to install.
2023-02-20 21:38:14 +00:00
ArcandGitHub b31371a070 Merge pull request #1510 from lnbits/default_ext_install
Works great!
2023-02-20 21:37:57 +00:00
Ben Arc 049b16a3fc format 2023-02-20 21:19:29 +00:00
Ben Arc cee4cfca43 added v-if 2023-02-20 18:53:43 +00:00
Ben Arc 81e4cfb6f4 Users need to be able to see "add extensions", even if they don't have extensions 2023-02-20 18:03:20 +00:00
callebtc bb26f2c7f0 Merge branch 'main' into default_ext_install 2023-02-20 16:30:30 +01:00
calleandGitHub 0b7e4269be fix gitignore (#1527)
* fix gitignore

* fix
2023-02-20 16:26:35 +01:00
ca75b328a1 remove invoices (#1515)
* remove invoices

* remove invoices tests

* remove unused tests/extensions dir

* fix issue

* formatting

* test passing locally. and i dont know why

* fix _extension_dir loading

* gitkeep extension dir

* formatting

* remove bundle from check

* do not prettier compliled css

* do not prettier compliled css

* create extension dir, if it does not exists, just to be sure

* Update lnbits/extension_manager.py

---------

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-02-20 15:34:18 +01:00
dni ⚡ bb05703f2b change prettier and reformat stuff 2023-02-20 11:49:00 +01:00
dni ⚡ ca33c276d3 remove watchonly 2023-02-20 11:49:00 +01:00
dni ⚡andGitHub 6dade2edf3 remove satspay (#1520) 2023-02-20 11:23:43 +01:00
dni ⚡andGitHub 5a8db02c60 remove tipjar (#1522) 2023-02-20 11:06:59 +01:00
dni ⚡andGitHub 4211998959 remove lnaddress (#1525)
* remove lnaddress
2023-02-20 09:08:23 +01:00
dni ⚡andGitHub 823956395b remove scrub (#1524) 2023-02-17 18:15:02 +01:00
dni ⚡andGitHub 4494f473a6 remove lndhub (#1523) 2023-02-17 18:03:31 +01:00
dni ⚡andGitHub 451c8f7a9e remove bleskomat (#1521) 2023-02-17 16:06:04 +01:00
dni ⚡andGitHub c49b68600e remove streamalerts (#1503) 2023-02-17 15:23:08 +01:00
ArcandGitHub d89de02864 Remove deezy (#1513) 2023-02-17 15:22:39 +01:00
1026b242ba Removed lnurlp (#1511)
* Removed lnurlp
* comment out extension checks for lnurlp

---------

Co-authored-by: dni  <office@dnilabs.com>
2023-02-17 15:21:53 +01:00
dni ⚡andGitHub b5be96836c remove ngrok (#1514) 2023-02-17 15:20:21 +01:00
dni ⚡andGitHub b2c580268e remove market (#1516) 2023-02-17 15:20:05 +01:00
dni ⚡andGitHub 1eb3df60fc remove withdraw (#1517) 2023-02-17 15:19:39 +01:00
dni ⚡andGitHub 967f46f82a remove paywall (#1518) 2023-02-17 15:18:39 +01:00
ArcandGitHub dd14d2dc54 Merge pull request #1512 from lnbits/removecashu
remove cashu
2023-02-16 15:56:36 +00:00
ben 10da63f6d4 remove cashu 2023-02-16 15:48:22 +00:00
ArcandGitHub 8637e7fd22 Merge pull request #1509 from lnbits/stop_extension_background_work
Extension Upgrade - stop background work
2023-02-16 10:01:15 +00:00
Vlad Stan 4345361847 chore: code clean-up 2023-02-16 10:48:27 +02:00
Vlad Stan 61fb44a04e feat: add default extension install (LNBITS_EXTENSIONS_DEFAULT_INSTALL) 2023-02-16 10:48:12 +02:00
Vlad Stan d32ad082c7 chore: fix typos 2023-02-15 17:32:49 +02:00
Vlad Stan 7ef4c18b56 doc: update coments 2023-02-15 17:31:10 +02:00
Vlad Stan ca28420e48 chore: code format 2023-02-15 17:26:08 +02:00
Vlad Stan e3ac700005 fix: extension DELETE call 2023-02-15 17:25:58 +02:00
ArcandGitHub a0f1f5d846 Merge pull request #1499 from lnbits/remove-boltz
remove boltz
2023-02-15 14:11:42 +00:00
ArcandGitHub 0b3f34fbbe Merge pull request #1500 from lnbits/remove-subdomains
remove subdomains
2023-02-15 14:08:51 +00:00
Vlad Stan 6540cd9d76 feat: add endpoint to stop background tasks 2023-02-15 15:45:28 +02:00
Vlad Stan 6a5afe3025 feat: stop extension background work on uninstall or upgrade 2023-02-15 15:34:09 +02:00
ArcandGitHub ef3beaaccf Merge pull request #1505 from lnbits/removesatsdiceagain
Remove satsdice...again
2023-02-15 10:59:00 +00:00
ben 0e1dbfcd26 again 2023-02-15 10:44:13 +00:00
ArcandGitHub 61ed49761f Merge pull request #1504 from lnbits/remove-livestream
remove livestream
2023-02-15 10:42:45 +00:00
ArcandGitHub 38d72408fb Merge pull request #1502 from lnbits/remove-offlineshop
remove offlineshop
2023-02-15 10:42:03 +00:00
dni ⚡ 6df8494ed6 remove livestream 2023-02-15 10:58:36 +01:00
dni ⚡ 755b032f17 remove offlineshop 2023-02-15 10:46:39 +01:00
dni ⚡ e1d9bf31d1 remove subdomains 2023-02-15 10:17:13 +01:00
dni ⚡ 7baa248204 remove boltz 2023-02-15 10:06:21 +01:00
ArcandGitHub 5bc2646103 Merge pull request #1498 from lnbits/remove-splitpayments
Remove splitpayments
2023-02-15 08:57:55 +00:00
ben 624a9ef60a Remove splitpayments 2023-02-14 14:45:47 +00:00
ArcandGitHub 891227b279 Merge pull request #1495 from lnbits/remove-hivemind
Remove hivemind
2023-02-14 14:37:04 +00:00
ben b9b84eda62 Merge remote-tracking branch 'origin/remove-hivemind' into remove-hivemind 2023-02-14 14:30:35 +00:00
ben 440818c219 Removes hivemind 2023-02-14 14:30:12 +00:00
ArcandGitHub c387e31330 Merge pull request #1494 from lnbits/removes-lnurldevice
removes lnurldevice
2023-02-14 14:24:55 +00:00
ArcandGitHub e4206bc6ec Merge pull request #1486 from lnbits/remove-smtp
remove smtp
2023-02-14 14:15:35 +00:00
ArcandGitHub 7c4ef0ec0f Merge pull request #1488 from lnbits/remove-discordbot
remove discord bot
2023-02-14 14:15:00 +00:00
ArcandGitHub 2fe1f4b581 Merge pull request #1493 from lnbits/remov-jukebox
Removed jukebox
2023-02-14 14:14:31 +00:00
ben b938103a07 removes lnurldevice 2023-02-14 14:14:07 +00:00
ben e1d4582345 Removed jukebox 2023-02-14 14:07:53 +00:00
ArcandGitHub 7a57690bd7 Merge pull request #1492 from lnbits/remove-nostrnip5
Removed nostrnip5
2023-02-14 13:48:22 +00:00
ben f23efaf750 Removed nostrnip5 2023-02-14 13:09:19 +00:00
ArcandGitHub a3ed1af9d5 Merge pull request #1491 from lnbits/remove-boltcards
Remove boltcards
2023-02-14 12:43:08 +00:00
ben 4431e9ce11 Remove boltcards 2023-02-14 12:23:17 +00:00
dni ⚡ f9ee6041ee remove discord bot 2023-02-14 13:08:23 +01:00
dni ⚡andGitHub db97ef5543 reintroduce extension migration check (#1490)
* reintroduce extension migration check

* introduce checkversions again and remove extensions from dbversions table
2023-02-14 13:06:01 +01:00
dni ⚡andGitHub 3276347d5d remove all extensions from migration data at once (#1489)
* remove all extnesions from migration data at once

* nerf workflow for now

* check dbversions
2023-02-14 12:42:46 +01:00
dni ⚡ a1a85d86ef remove hivemind placeholder extension 2023-02-14 11:35:30 +01:00
dni ⚡ cb475d7f88 remove smtp 2023-02-14 10:50:25 +01:00
calleandGitHub 77a6ac041c Cashu: add LNURLp (#1484) 2023-02-13 16:24:15 +01:00
ArcandGitHub 9840a8b154 Merge pull request #1481 from lnbits/removesevents
Removes events
2023-02-11 10:13:05 +00:00
ben 82add2189a Removes events 2023-02-11 08:53:24 +00:00
ArcandGitHub 252ff65563 Merge pull request #1478 from lnbits/arcbtc-patch-8
Update README.md
2023-02-10 14:49:42 +00:00
ArcandGitHub fd3f32ad51 Update README.md 2023-02-10 13:19:59 +00:00
ArcandGitHub 592f2b6afa Merge pull request #1476 from lnbits/removegerty
Pulled gerty
2023-02-10 13:04:33 +00:00
ben b99b413499 Pulled gerty 2023-02-10 12:50:49 +00:00
ArcandGitHub 741f1a3daf Merge pull request #1469 from lnbits/pyright4-extensions-only
pyright but only for extensions
2023-02-10 09:49:00 +00:00
dni ⚡ c9e027ca21 removed unused deps 2023-02-08 08:35:46 +01:00
dni ⚡andGitHub b151004476 fix/unused deps (#1473)
* remove unused packages

fixup

* typo in log
2023-02-07 18:09:22 +01:00
ben eb54d6fe92 Less fancy, but working db call 2023-02-07 14:47:03 +00:00
ben 96a66376bd Cowboy fix for lnurlpay success action
Pyment hash is still sent https://webhook.site/#!/8e4e4541-0ea3-4ede-8765-68ff261f5c97/ff4136b6-220d-4b35-b865-5a19fa924947/1, so these lines are prob not needed
2023-02-07 11:46:17 +00:00
dni ⚡ 538f44e6f0 lnadress, fixes for vlad :) 2023-02-07 10:31:05 +01:00
dni ⚡ 9f6dc18be7 subdomain formatting 2023-02-07 10:17:01 +01:00
dni ⚡ 2fb8eb505a try catch subdomain invoice_listener 2023-02-07 10:11:50 +01:00
dni ⚡ e0d9f2d764 fixes for vlad :) 2023-02-07 09:48:11 +01:00
dni ⚡ f547b1f141 add assert message for streamalerts crud 2023-02-07 09:20:05 +01:00
dni ⚡ 8bf4103523 order of make check 2023-02-07 08:55:52 +01:00
dni ⚡ 05bb502bf5 fix watchonly 2023-02-07 08:35:48 +01:00
calleandGitHub 9467a8da07 Cashu: fix decode QR link (#1472) 2023-02-06 18:00:31 +01:00
calleandGitHub 4593f7a942 Cashu: fix recv_token param (#1471) 2023-02-06 17:55:23 +01:00
calleandGitHub 479ca16118 Cashu: fix mint init (#1470) 2023-02-06 17:44:46 +01:00
dni ⚡ 16679c0d65 exclude core from pyright 2023-02-06 13:41:26 +01:00
Pavol Rusnakanddni ⚡ 3bd68cb394 fix pyright lnbits/extensions
Co-authored-by: dni  <office@dnilabs.com>
2023-02-06 13:35:38 +01:00
Pavol Rusnakanddni ⚡ 09eab69e12 fix pyright in Makefile, add pyright+pylint+flake8 to check target
Co-authored-by: dni  <office@dnilabs.com>
2023-02-06 13:31:42 +01:00
Pavol Rusnakanddni ⚡ b8a5cc60b8 add pyright workflow, unify mypy+flake8+pylint workflows
Co-authored-by: dni  <office@dnilabs.com>
2023-02-06 13:31:42 +01:00
calleandGitHub c117ad6aa4 Cashu: mint selection UI (#1467) 2023-02-06 00:28:34 +01:00
calleandGitHub edc5531d24 Cashu: Multimint swaps (#1466)
* Cashu: Multimint swaps

* clean up comments

* clean up comments
2023-02-05 23:56:36 +01:00
calleandGitHub 77e0dc0288 Cashu: mint adding notification (#1465) 2023-02-05 20:32:21 +01:00
calleandGitHub d41afe4236 Cashu: better error handling (#1464) 2023-02-05 20:25:08 +01:00
calleandGitHub 65ea2a05f9 Cashu: minor UI fixes (#1463) 2023-02-05 19:33:08 +01:00
calleandGitHub 97da27b6bb Cashu: minor UI fixes (#1462) 2023-02-05 19:13:36 +01:00
calleandGitHub a55e72f66f Cashu: minor UI fixes (#1460) 2023-02-05 18:22:58 +01:00
calleandGitHub d4d5cd887d Cashu: fix short url label (#1459) 2023-02-05 18:08:01 +01:00
calleandGitHub 67d246b675 Cashu: hide bottom bar on camera (#1458) 2023-02-05 17:48:57 +01:00
calleandGitHub 04d6656b27 Cashu: Fix bottom bar (#1457)
* Cashu: fix button label

* hide bottom on dialogs
2023-02-05 17:45:48 +01:00
calleandGitHub f03efd7a77 cashu: hide navbar on welcome dialog (#1456) 2023-02-05 16:17:33 +01:00
calleandGitHub 754db04d99 Cashu: PWA install button (#1455) 2023-02-05 14:04:23 +01:00
calleandGitHub 9f177dea98 cashu: fix balance on all mints label (#1454) 2023-02-05 12:49:42 +01:00
calleandGitHub 970a0ce1d0 Cashu: UI upgrades and fixes (#1453)
* Cashu: UI updates and automatic mint activation

* welcome dialog

* welcome dialog

* UI updates
2023-02-05 12:44:56 +01:00
calleandGitHub 9d2008d45c Cashu: fix multimint switch (#1452) 2023-02-04 05:12:44 +01:00
calleandGitHub bbbbb8b9af cashu: fix manifest (#1451) 2023-02-04 04:54:37 +01:00
calleandGitHub 661fc6c625 Cashu: fix UI bugs (#1450) 2023-02-04 04:17:30 +01:00
calleandGitHub 7968c495ea ☮️ peace (#1449)
* ☮️ peace

* cashu: fix default tab
2023-02-04 03:50:05 +01:00
calleandGitHub d01c5dfdec Cashu waket: multimint support (#1448)
* multimint working

* working

* fix typo

* migrations

* migration

* migration

* migration

* fix local storage

* notifications

* strings

* add mint dialog

* make format

* update favicons

* icons
2023-02-04 03:45:08 +01:00
calleandGitHub 0dca98ca2e Cashu fix: token spendable check (#1445) 2023-02-03 07:41:01 +01:00
ArcandGitHub 8c4dfb5d28 Merge pull request #1443 from lnbits/gitignore2
STOP DELETING!
2023-02-02 11:05:03 +00:00
ben 0da9cb0d64 STOP DELETING! 2023-02-02 10:56:05 +00:00
dni ⚡andGitHub 0dffb2c1a0 moving build file into tools, keep rootdir clean (#1434)
* moving build file into tools, keep rootdir clean

* formatting

* make flake happy
2023-02-01 17:19:56 +01:00
dni ⚡andGitHub 3ac6a98adc TEST: add helpers and a test to pay external invoice (#1414)
* add helpers and a test to pay external invoice

* skip real test in fakewallet

* formatting

* fix test

* formatting
2023-02-01 16:54:07 +01:00
e7802c27ab update .gitignore to ignore .toml (#1404)
* update .gitignore to ignore .toml

Prevent accidental .toml checkins for fly.io deploys!

* Update .gitignore

ignore fly.toml

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

* Update .gitignore

---------

Co-authored-by: dni  <office@dnilabs.com>
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-02-01 16:51:18 +01:00
ArcandGitHub 865f4712c4 Merge pull request #1440 from lnbits/noreloadinstall
Removes install page reloads
2023-02-01 06:37:03 +00:00
ben 31ae40d44d Working 2023-02-01 06:30:03 +00:00
ben d2d7bd6469 format 2023-02-01 06:13:01 +00:00
ben e20bb99811 Removes install page reloads 2023-02-01 06:12:00 +00:00
ArcandGitHub 2470497ce5 Merge pull request #1438 from lnbits/examplepull
Pulled example
2023-01-31 23:24:08 +00:00
ben 9b21e2028e Removed example from mock 2023-01-31 22:39:10 +00:00
ben b5c8633ab0 Removed example 2023-01-31 22:38:20 +00:00
ben 5f43ae4a2d Merge branch 'main' into examplepull 2023-01-31 22:37:44 +00:00
ArcandGitHub deb9825391 Merge pull request #1437 from lnbits/gitignore
Added gitignore extensions....again
2023-01-31 22:24:54 +00:00
ben 5c5f414c25 Added gitignore extensions....again 2023-01-31 22:14:39 +00:00
ArcandGitHub c45c6b8ae8 Merge pull request #1369 from prusnak/autoflake
add flake8 + add pylint + fix reported issues
2023-01-31 11:47:56 +00:00
Pavol Rusnak 58a7098be5 fix unused variable 2023-01-30 11:46:45 +00:00
Pavol Rusnak 430698c763 improve workflow stability for flake8 and pylint
followup to #1397
2023-01-30 11:46:45 +00:00
Pavol Rusnak 0ff4c13346 add flake8 to CI, skip remaining 4 flake8 issues (will be addressed later) 2023-01-30 11:46:45 +00:00
Pavol Rusnak 24fbb9ef5d fix flake8 F811 (redefinition of unused) 2023-01-30 11:46:45 +00:00
Pavol Rusnak 388cfc18fa fix flake8 F405 (may be undefined) 2023-01-30 11:46:45 +00:00
Pavol Rusnak a49a654137 fix flake8 F401+F403 (module imported but unused + unable to detect undefined names) 2023-01-30 11:46:45 +00:00
Pavol Rusnak 967ce06ca5 fix pylint C0206 (consider-using-dict-items) 2023-01-30 11:46:45 +00:00
Pavol Rusnak 3e42adc66f fix flake8 F601 (dictionary key repeated with different values) 2023-01-30 11:46:45 +00:00
Pavol Rusnak e05340269b fix flake8 F841 (local variable is assigned to but never used) 2023-01-30 11:46:45 +00:00
Pavol Rusnak 8a72585f00 fix flake8 W293 (blank line contains whitespace) 2023-01-30 11:46:44 +00:00
Pavol Rusnak a02f147482 fix flake8 E302 (expected 2 blank lines) 2023-01-30 11:46:44 +00:00
Pavol Rusnak 421214bc36 fix flake8 W291 (trailing whitespace) 2023-01-30 11:46:44 +00:00
Pavol Rusnak bc36e6bc4c fix flake8 F541 (f-string is missing placeholders) 2023-01-30 11:46:44 +00:00
Pavol Rusnak 86e8a3a315 fix flake8 E711 (comparison-to-none) 2023-01-30 11:46:44 +00:00
Pavol Rusnak eba7319808 fix flake8 E713 (test-for-membership) 2023-01-30 11:46:44 +00:00
Pavol Rusnak f6bd8684d3 fix flake8 E712 (comparison-to-bool) 2023-01-30 11:46:44 +00:00
Pavol Rusnak 9ef4bd8fb9 add pylint workflow after all enabled issues were fixed 2023-01-30 11:46:44 +00:00
Pavol Rusnak 50c613cdd4 silence pylint W0231 (super-init-not-called) 2023-01-30 11:46:44 +00:00
Pavol Rusnak a0cc55c5a1 fix pylint C0209 (consider-using-f-string) 2023-01-30 11:46:43 +00:00
Pavol Rusnak 9fa3e5c6cf fix pylint W0102 (dangerous-default-value) 2023-01-30 11:46:43 +00:00
Pavol Rusnak 906d91bcf1 fix pylint R1714 (consider-using-in) 2023-01-30 11:46:43 +00:00
Pavol Rusnak 35396d7e63 fix pylint R0205 (useless-object-inheritance) 2023-01-30 11:46:43 +00:00
Pavol Rusnak d29fcca6aa fix pylint W0612 (unused-variable) 2023-01-30 11:46:43 +00:00
Pavol Rusnak 32f4c8aa17 fix shadowing in build.py 2023-01-30 11:46:43 +00:00
Pavol Rusnak 15d7fbeace run autoflake
```
autoflake -i --remove-unused-variables $(find . -name "*.py")
```
2023-01-30 11:46:43 +00:00
Pavol Rusnak c8efe41e1a add flake8 and pylint as dev dependencies 2023-01-30 11:46:43 +00:00
calleandGitHub 52e654af86 respect https setting in admin ui link (#1416)
* respect https setting in admin ui link

* make format
2023-01-30 11:30:23 +01:00
calleandGitHub 80a94aa9ce Fix/cashu update protocol (#1433)
* updates NOTE: fix pyproject and requirements

* revert GET to POST

* update cashu on pip and change name of models

* adjust client to spendable

* refactor spendable
2023-01-30 09:29:44 +01:00
ArcandGitHub bdfbbb779e Merge pull request #1394 from lnbits/feat/boltz-fee-info
feat/boltz fee info
2023-01-30 00:57:07 +00:00
ArcandGitHub 2aa569b4a8 Merge pull request #1426 from lnbits/usermanagerpull
Removes Usermanager
2023-01-29 23:55:18 +00:00
ben 817dd4083f Merge branch 'main' into usermanagerpull 2023-01-29 23:47:00 +00:00
ArcandGitHub aefbced978 Merge pull request #1427 from lnbits/fixinstallfeatured
Forces featured first
2023-01-29 23:42:37 +00:00
ArcandGitHub 4cfda287bf Merge pull request #1432 from lnbits/revert-1429-fix/gitignore_revert
Revert "fix gitignore"
2023-01-29 23:05:42 +00:00
ArcandGitHub f1ef9a58fe Revert "fix gitignore (#1429)"
This reverts commit ff3f86afa6.
2023-01-29 22:59:38 +00:00
dni ⚡andGitHub b2a963638d TEST: update actions setup-python to v4 and checkout to v3, add caching to python (#1428)
* update actions setup-python to v4 and checkout to v3, add caching to python

* fix ordering of steps for poetry to be cached

* poetry setup ordering

* order in formatting.yml
2023-01-29 11:46:20 +01:00
calleandGitHub ff3f86afa6 fix gitignore (#1429) 2023-01-29 11:25:43 +01:00
ben 7ea3d6c245 format 2023-01-28 21:53:00 +00:00
ben f9d068cf2c Forces featured first 2023-01-28 21:48:27 +00:00
ben 0668fd79b6 Removed usermanager 2023-01-28 21:01:01 +00:00
ArcandGitHub f11044ae75 Merge pull request #1424 from lnbits/tpospull
Pulls tpos for install worflow
2023-01-27 14:26:57 +00:00
ben 432ca98663 Pulled tpos 2023-01-27 13:03:21 +00:00
ArcandGitHub b1115e5b11 Merge pull request #1423 from lnbits/pullsupport
Removed LNticket
2023-01-27 12:42:31 +00:00
ben f22e5069df Removed lnticket 2023-01-27 12:16:54 +00:00
ArcandGitHub 6846dd373f Merge pull request #1231 from lnbits/extension_install_02
Extension install 02
2023-01-26 17:01:52 +00:00
calleandGitHub 86d1ce2d6e merely updates the version tag in pyproject.toml (#1421) 2023-01-26 14:28:10 +01:00
calleandGitHub 12d037901e Dependencies: update protobuf and grpcio to latest versions (#1420)
* update protobuf and grpcio to latest versions

* fix their versions
2023-01-26 14:24:17 +01:00
dni ⚡ 1c3a9d1881 remove lockup_tx from exmaple equation 2023-01-26 14:08:24 +01:00
ben e4479fcb78 removed copilot from mock data 2023-01-26 12:51:13 +00:00
calleandGitHub 9a39a5d5ba fix all library versions (#1419) 2023-01-26 13:51:12 +01:00
ben 39f0000fa5 Merge remote-tracking branch 'origin/main' into extension_install_02 2023-01-26 12:39:11 +00:00
calleandGitHub f6e0301209 Dependencies: fix cashu and boltz version (#1418)
* fix cashu version

* fix boltz version
2023-01-26 13:35:27 +01:00
dni ⚡andGitHub 4ccfe6dfa8 DOC: update install docker docs (#1415) 2023-01-26 11:53:59 +01:00
ben 7366200571 Merge remote-tracking branch 'origin/extension_install_02' into extension_install_02 2023-01-26 10:32:38 +00:00
ben 850169b3bd Ignore extensions and upgrades 2023-01-26 10:32:00 +00:00
Vlad Stan 6276a7a4db fix: icon missing after extension install 2023-01-26 12:30:42 +02:00
f0d58a8365 Wallets: add custom invoice expiry (#1396)
* expiry for fakewallet

* expiry for lnd

* lnbits backend

* fix: eclair descriptionHash fixed and expiry added

* cln and sparko

* test expiry

* Eclair from AdminUI and bugfix for nonexistent payments

* add to settings and .env and remove lntxbot

* remove duplicate and format

* add invoice expiry

* add min max and step

* UI works now

* test should fail, sanity check, will revert

* revert, ready for merge

Co-authored-by: Tiago Vasconcelos <talvasconcelos@gmail.com>
2023-01-26 11:08:40 +01:00
5a0b217d63 TEST: LNbits as fundingsource in regtest (#1402)
* lnbits funding source

* add create_fake_user to workflow

* change quotes in workflow ymal

* not use interactive docker exec

* update tools/create_fake_admin to not use lnbits imports

* formatting

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2023-01-26 10:43:12 +01:00
ben a2fc34c08e reworded 2023-01-25 21:45:54 +00:00
ben 1df9664937 Added notify for non admins 2023-01-25 21:45:01 +00:00
ben 1a20d35a13 format 2023-01-25 21:25:49 +00:00
ben 75e8683fb1 Default featured 2023-01-25 21:24:59 +00:00
ben 9bf2c231f8 5 stars! 2023-01-25 21:21:36 +00:00
ben 8d64c6d877 Adde trim to manifest upload 2023-01-25 21:20:20 +00:00
ben 880c955208 I think its fixed, although stoopid imagur not working locally 2023-01-25 21:07:11 +00:00
ben 925d4ccbe7 Removed streamer copilot, as we need a demo 2023-01-25 20:58:28 +00:00
ben ad70e9c116 Remove choice all icons will be tiles 2023-01-25 20:51:57 +00:00
ben cc3dbaa1a7 working again 2023-01-25 20:29:42 +00:00
ben fd71c82183 gave up on seperate js install file 2023-01-25 20:28:05 +00:00
ben 6bd2a071ec Merge remote-tracking branch 'origin/main' into extension_install_02 2023-01-25 18:43:44 +00:00
ben efa24584f3 Broken, trying to take js out 2023-01-25 17:33:24 +00:00
ben 2eef5028f9 Replaced tiles, for those who have them 2023-01-25 16:57:08 +00:00
ben 50458901d6 Added generic icon for iconless plugiins 2023-01-25 16:55:08 +00:00
ben c344a70de9 tab tweak 2023-01-25 15:34:10 +00:00
ben 8bae8b06ea Disabled rating 2023-01-25 15:29:06 +00:00
ben 953bca0cf5 Spelling/tweaked card 2023-01-25 15:09:15 +00:00
dni ⚡andGitHub 36c881c4ce add eclair fundingsource workflow / test (#1406) 2023-01-25 15:07:41 +01:00
Vlad Stan 4af80858bc feat: add page titles 2023-01-25 15:49:50 +02:00
Vlad Stan 41a0a2d66e fix: icon size on mobile 2023-01-25 15:37:28 +02:00
Vlad Stan 00c02e3c85 fix: tabs on mobile 2023-01-25 15:28:06 +02:00
Vlad Stan 8ab4cb8c08 fix: restore installed extensions 2023-01-25 15:00:39 +02:00
Vlad Stan 46faa62b8b fix: upgrade vs install 2023-01-25 14:32:41 +02:00
calleandGitHub 45a0657828 do not reference these object (#1412) 2023-01-25 13:24:36 +01:00
calleandGitHub 1c4414d311 await ledeger verify (#1411) 2023-01-25 13:14:52 +01:00
Vlad Stan a242a823fc fix: do not propagate technical message to the user 2023-01-25 12:46:45 +02:00
Vlad Stan 1373b88169 fix: stars look&feel 2023-01-25 11:48:57 +02:00
Vlad Stan 9c105e8885 chore: code format 2023-01-25 11:19:11 +02:00
Vlad Stan f27e5af639 feat: add tabs to install all/featured/installed 2023-01-25 11:15:56 +02:00
Vlad Stan b016804a5b fix: change star color 2023-01-25 10:05:30 +02:00
Vlad Stan dd3fc7c2bb fix: merge errors 2023-01-25 09:56:05 +02:00
Vlad StanandGitHub 9cca87f738 Merge branch 'main' into extension_install_02 2023-01-25 09:50:54 +02:00
dni ⚡ afe43b6bd4 dynamic fee calc 2023-01-24 11:23:09 +01:00
dni ⚡ b197a87eed change boltz fee in example 2023-01-24 11:23:09 +01:00
dni ⚡ 302ed83051 add boltz fee dynamically 2023-01-24 11:23:09 +01:00
dni ⚡ 994070a7a5 update boltz_client to 0.1.3 2023-01-24 11:23:09 +01:00
Vlad Stan 8971fb5f00 feat: add default value for lnbits_extensions_manifests 2023-01-23 14:58:32 +02:00
Vlad Stan 5ae1a22989 fix: order of declarations 2023-01-23 12:08:44 +02:00
Vlad Stan 567c636b1d refactor: re-order classes 2023-01-23 12:00:09 +02:00
Vlad Stan 39add25b3b refactor use pydantic 2023-01-23 11:52:15 +02:00
Vlad Stan b57e808198 chore: add extra log 2023-01-23 11:51:53 +02:00
Vlad Stan 6b678186a2 chore: remove bad file 2023-01-20 18:44:19 +02:00
Vlad Stan ca1dcd6768 chore: comment typos 2023-01-20 18:26:58 +02:00
Vlad Stan 0b3324cd8f refactor: create TransientSettings for settings that are not to be persisted 2023-01-20 18:10:29 +02:00
Vlad Stan 2dff48dcd0 chore: fix comment 2023-01-20 18:01:05 +02:00
Vlad Stan 5212320d8d refactor: simplify if+for 2023-01-20 18:00:50 +02:00
Vlad Stan e95c6ec3a6 chore: add docs to InstalledExtensionMiddleware class 2023-01-20 17:58:56 +02:00
Vlad Stan b069fe17d8 chore: add comments for register_new_ext_routes 2023-01-20 17:48:52 +02:00
Vlad Stan 4530400d33 chore: add types to method params 2023-01-20 17:08:44 +02:00
Vlad Stan f75529e1c2 fix: double assignment 2023-01-20 16:11:51 +02:00
Vlad Stan 53a95ccc11 refactor: search in list 2023-01-20 16:11:21 +02:00
Vlad Stan 5b6e11a621 fix: import 2023-01-20 16:10:47 +02:00
Vlad Stan fa5d824eea refactor: remove redundant changes 2023-01-20 15:59:44 +02:00
Vlad Stan ea31e148ae refactor: simplify download & extract logic 2023-01-20 15:55:58 +02:00
Vlad Stan 9cf587a64a chore: format SQL 2023-01-20 15:53:27 +02:00
Vlad Stan 8857cb94b4 feat: move GitHub stars inside the repos list 2023-01-20 15:52:27 +02:00
Vlad Stan cf4c9baeeb feat: make page more similar with the extensions page 2023-01-20 15:08:48 +02:00
Vlad Stan 223e92f9e2 fix: make layout similar with extensions page 2023-01-20 14:41:37 +02:00
Vlad Stan a2284da4c3 feat: use Add Extension menu item 2023-01-20 14:33:45 +02:00
Vlad StanandGitHub 30d23c2bef doc: fix release view 2023-01-20 11:46:17 +02:00
Vlad Stan 177d17ec10 chore: remove log statemet 2023-01-20 11:32:39 +02:00
Vlad Stan 43298dfcc7 Merge branch 'extension_install_02' of https://github.com/lnbits/lnbits into extension_install_02 2023-01-20 10:48:28 +02:00
Vlad StanandGitHub 77312d5e3c doc: update Uninstall section 2023-01-20 10:47:47 +02:00
Vlad StanandGitHub 12790dec5d doc: start Unistall Extension section 2023-01-20 10:42:14 +02:00
Vlad StanandGitHub c2ca9403e4 doc: add note 2023-01-20 10:38:38 +02:00
Vlad StanandGitHub e12211e4c5 doc: install dialog 2023-01-20 10:21:19 +02:00
Vlad Stan 9a002a2342 Merge branch 'extension_install_02' of https://github.com/lnbits/lnbits into extension_install_02 2023-01-20 10:06:42 +02:00
Vlad Stan 730017ed62 fix: import after renaming 2023-01-20 10:06:32 +02:00
Vlad StanandGitHub 0b0efd99c1 doc: minor changes 2023-01-20 10:03:56 +02:00
Vlad Stan 776acdd857 fix: typo in file name 2023-01-20 10:00:09 +02:00
Vlad Stan 478d74efc8 fix: separate lnbits_deactivated_extensions from lnbits_disabled_extensions` 2023-01-20 09:39:27 +02:00
Vlad StanandGitHub dea63ca2db doc: add repo extension field details 2023-01-19 18:57:29 +02:00
Vlad StanandGitHub 37ec55a6e4 doc: add github repo example 2023-01-19 18:49:34 +02:00
Vlad StanandGitHub d8ba9448a2 doc: collapse Fields Detailed Description table 2023-01-19 18:45:17 +02:00
Vlad StanandGitHub c22b2b7dfc doc: add Explicit Relase field description 2023-01-19 18:42:32 +02:00
Vlad StanandGitHub 17888ebb49 doc: add config repo steps 2023-01-19 18:17:23 +02:00
Vlad StanandGitHub 496020e47a Create extension-install.md 2023-01-19 18:04:20 +02:00
Vlad Stan 1fb4c1a3b6 fix: icon on deactivate 2023-01-19 16:44:36 +02:00
Vlad Stan 0a7a589935 fix: call load_disabled_extension_list 2023-01-19 15:49:43 +02:00
Vlad Stan bb32112169 fix: icon_url 2023-01-19 14:47:59 +02:00
Vlad Stan 232a9e9885 fix: UI fixes 2023-01-19 12:34:10 +02:00
Vlad Stan db73a51c74 feat: allow regular users to view the install page 2023-01-19 12:16:44 +02:00
Vlad Stan 4d87246811 feat: store icon, icon_url and name for installed extensions 2023-01-19 11:57:17 +02:00
Vlad Stan 6773f33ad1 fix: re-mount routes for restored extensions 2023-01-19 11:12:48 +02:00
Vlad Stan aa006b66d1 feat: update pre-installed extensions on re-create 2023-01-19 10:15:56 +02:00
Vlad Stan 69b8dfa21f fix: mypy 2023-01-18 18:40:27 +02:00
Vlad Stan ce1639792a fix: order of migrations 2023-01-18 18:35:02 +02:00
Vlad Stan d3c05ef300 doc: add LNBITS_EXT_GITHUB_TOKEN 2023-01-18 17:38:51 +02:00
Vlad Stan e52baab1c2 fix: register extension route after re-install 2023-01-18 17:38:51 +02:00
Vlad Stan 98735d79c7 fix: revert onstartup extension route init 2023-01-18 17:38:51 +02:00
Vlad Stan 1d2e994862 feat: download extensions if missing 2023-01-18 17:38:51 +02:00
Vlad Stan 59bc9804ad chore: clean-up 2023-01-18 17:38:51 +02:00
Vlad Stan 72965c8d85 chore: clean-up 2023-01-18 17:38:51 +02:00
Vlad Stan 040cd390c8 feat: UI improvements 2023-01-18 17:38:51 +02:00
Vlad Stan 3295c6bebe feat: UI improvements 2023-01-18 17:38:51 +02:00
Vlad Stan d89128fb93 feat: show release page link 2023-01-18 17:38:51 +02:00
Vlad Stan f379b784cc chore: refactor 2023-01-18 17:38:51 +02:00
Vlad Stan 12d32a4811 feat: add GITHUB_TOKEN for github api calls (prevent rate-limit) 2023-01-18 17:38:51 +02:00
Vlad Stan 66d3e2fb2d feat: add label to repo 2023-01-18 17:38:51 +02:00
Vlad Stan 5bcb8cb349 fix: deactivate on upgrade 2023-01-18 17:38:51 +02:00
Vlad Stan b957b506f9 fix: close dialog on install 2023-01-18 17:38:51 +02:00
Vlad Stan b039ab1e6b chore: code clean-up 2023-01-18 17:38:51 +02:00
Vlad Stan a950af9944 fix: quasar for-each error 2023-01-18 17:38:51 +02:00
Vlad Stan 975bbc2633 chore: add extra logs 2023-01-18 17:38:51 +02:00
Vlad Stan 01b5536f3a feat: Uninstall only installedextensions 2023-01-18 17:38:51 +02:00
Vlad Stan 0195c242c2 feat: add is isAvailable flag 2023-01-18 17:38:51 +02:00
Vlad Stan 584b667637 feat: show new install available icon 2023-01-18 17:38:51 +02:00
Vlad Stan 03df382325 feat: fetch the installed release 2023-01-18 17:38:51 +02:00
Vlad Stan ce8d45c8c6 feat: only allow activation if installed 2023-01-18 17:38:51 +02:00
Vlad Stan ebbfdbb6c9 feat: use manage dialog 2023-01-18 17:38:51 +02:00
Vlad Stan 86c86958ae refactor: more and more 2023-01-18 17:38:51 +02:00
Vlad Stan 77c17a2b63 chore: fix formatting 2023-01-18 17:38:51 +02:00
Vlad Stan 6b360b927c feat: returned the installed release 2023-01-18 17:38:51 +02:00
Vlad Stan 12060b67ad feat: store and retrieved the installed_release 2023-01-18 17:38:36 +02:00
Vlad Stan d146a45b59 refactor: move logic to static method get_extension_release 2023-01-18 17:38:36 +02:00
Vlad Stan 980267cb51 chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 9d0cedfcb2 feat: install GitHib releases also 2023-01-18 17:38:36 +02:00
Vlad Stan 41ce316fc6 feat: add install details 2023-01-18 17:38:36 +02:00
Vlad Stan e8bed9cd90 feat: UI improvements 2023-01-18 17:38:36 +02:00
Vlad Stan 2da466f0c8 chore: clean-up 2023-01-18 17:38:36 +02:00
Vlad Stan dd0c97eefc fix: make github releases g first 2023-01-18 17:38:36 +02:00
Vlad Stan 86d45cdb50 feat: fetch releases for GitHub repo 2023-01-18 17:38:36 +02:00
Vlad Stan 27b308ae1a feat: separate extension release into new API 2023-01-18 17:38:36 +02:00
Vlad Stan a73e8ae44d feat: stub for extension versions 2023-01-18 17:38:36 +02:00
Vlad Stan 0b49541c15 chore: renamings 2023-01-18 17:38:36 +02:00
Vlad Stan eb88592473 feat: addd basic version install UI 2023-01-18 17:38:36 +02:00
Vlad Stan c9bb5a660c feat: show empty Upgrade dialog 2023-01-18 17:38:36 +02:00
Vlad Stan 97ea78f999 fix: return only the latest release 2023-01-18 17:38:36 +02:00
Vlad Stan 522bfcf4ae chore: optimize imports 2023-01-18 17:38:36 +02:00
Vlad Stan b4408e9d52 chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 9a77a6e314 fix: exclude draft and prerelease releases 2023-01-18 17:38:36 +02:00
Vlad Stan 76fbb5ccbb feat: return releases to UI 2023-01-18 17:38:36 +02:00
Vlad Stan 496346b3ba feat: use github releases for installing extensions 2023-01-18 17:38:36 +02:00
Vlad Stan 38a132604b feat: used dedicated table for installed extensions 2023-01-18 17:38:36 +02:00
Vlad Stan 9bd1720d03 chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 032a000da7 fix: do not fail when one of the extension manifest files is not available 2023-01-18 17:38:36 +02:00
Vlad Stan 4efeae24d0 feat: allow extension manifest configuration from the Admin UI 2023-01-18 17:38:36 +02:00
Vlad Stan 9d0fba7c15 fix: clean upgrades folder on uninstall 2023-01-18 17:38:36 +02:00
Vlad Stan e3ee04edc7 refactor: code clean-up 2023-01-18 17:38:36 +02:00
Vlad Stan de929c6c70 refactor: extract more methods into InstallableExtension 2023-01-18 17:38:36 +02:00
Vlad Stan 84d2e93119 chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 1ca0cf4313 refactor: use factory method 2023-01-18 17:38:36 +02:00
Vlad Stan 0ab913e502 chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 2edb9da387 fix: name conflict 2023-01-18 17:38:36 +02:00
Vlad Stan 795d05c109 refactor: extract extensions.py 2023-01-18 17:38:36 +02:00
Vlad Stan e7a150e708 refactor: move more logic to InstallableExtension 2023-01-18 17:38:36 +02:00
Vlad Stan 1b07768b76 refactor: use check_admin decorator 2023-01-18 17:38:36 +02:00
Vlad Stan 3ed2b3cdeb refactor: move more logic to InstallableExtension 2023-01-18 17:38:36 +02:00
Vlad Stan cb6349fd76 refactor: extract dome methods to helpers 2023-01-18 17:38:36 +02:00
Vlad Stan cae71faf37 refactor: use the new check_admin 2023-01-18 17:38:36 +02:00
Vlad Stan ece7a82b3d fix: make function async 2023-01-18 17:38:36 +02:00
Vlad Stan 8f77c3f7bf fix: handle no-path scenario 2023-01-18 17:38:36 +02:00
Vlad Stan 59d8cff1da fix: avoid conflicts with dbversions table from cashu schema 2023-01-18 17:38:36 +02:00
Vlad Stan cb24a6a4f9 fix: rating star size 2023-01-18 17:38:36 +02:00
Vlad Stan a2201c9d31 chore: add extra log 2023-01-18 17:38:36 +02:00
Vlad Stan 51c8e183d1 fix: add module_name property 2023-01-18 17:38:36 +02:00
Vlad Stan 6ec1a668ae chore: code format 2023-01-18 17:38:36 +02:00
Vlad Stan 8c1479427d fix: merge issues 2023-01-18 17:38:36 +02:00
Vlad Stan 9dfeef15fd fix: merge issue 2023-01-18 17:38:36 +02:00
Vlad Stan 76cfaf93ad fix: merge issue 2023-01-18 17:38:36 +02:00
Vlad Stan bdee9f46e3 refactor: put classes together 2023-01-18 17:38:36 +02:00
Vlad Stan 691e175370 feat: add re-routing for upgraded extension APIs 2023-01-18 17:38:36 +02:00
Vlad Stan 0d61db13ec fix: handle first time install different than subsequent updates 2023-01-18 17:38:36 +02:00
Vlad Stan a2d35f65c9 fix: enforce unique key in v-for 2023-01-18 17:38:36 +02:00
Vlad Stan 08be621529 fix: migration module name 2023-01-18 17:38:36 +02:00
Vlad Stan a6e3b5e049 feat: use hash for unique extension id 2023-01-18 17:38:36 +02:00
Vlad Stan 8f8ebbeeaa refactor: class rename and code format 2023-01-18 17:38:36 +02:00
Vlad Stan 598d2a6939 feat: re-route trafic when an extension is upgraded 2023-01-18 17:38:36 +02:00
Vlad Stan 3fbac927e9 chore: clean-up 2023-01-18 17:38:36 +02:00
Vlad Stan 6c41ac841b feat: temporary fix for upgrades 2023-01-18 17:38:36 +02:00
Vlad Stan 11815958a3 chore: remove test code 2023-01-18 17:38:36 +02:00
Vlad Stan 2d3ba2c457 fix: UI position 2023-01-18 17:38:36 +02:00
Vlad Stan 66c908e600 chore: migrate after major changes on main 2023-01-18 17:38:36 +02:00
Vlad Stan 331e93195d chore: ignore the data dir 2023-01-18 17:38:24 +02:00
Vlad Stan 1f4f4d93ca feat: add LNBITS_EXTENSIONS_MANIFESTS env var 2023-01-18 17:38:24 +02:00
787 changed files with 86237 additions and 80179 deletions
-2
View File
@@ -3,8 +3,6 @@ data
docker
docs
tests
venv
tools
lnbits/static/css/*
lnbits/static/bundle.js
+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
+46 -17
View File
@@ -8,12 +8,29 @@ PORT=5000
DEBUG=false
# 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"
# 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=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Disable account creation for new users
# LNBITS_ALLOW_NEW_ACCOUNTS=false
# 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.
@@ -30,8 +47,18 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Hides wallet api, extensions can choose to honor
LNBITS_HIDE_API=false
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
# 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://...
@@ -41,28 +68,33 @@ LNBITS_DISABLED_EXTENSIONS="amilk"
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"
# value in millisats
LNBITS_RESERVE_FEE_MIN=2000
# value in percent
LNBITS_RESERVE_FEE_PERCENT=1.0
# Limit fiat currencies allowed to see in UI
# LNBITS_ALLOWED_CURRENCIES="EUR, USD"
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador"
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet,
# LndWallet, LndRestWallet, CoreLightningWallet, EclairWallet,
# LnTipsWallet, LNbitsWallet, SparkWallet, FakeWallet,
# which fundingsources are allowed in the admin ui
LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, 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=3600
# Set one of these blocks depending on the wallet kind you chose above:
# ClicheWallet
@@ -75,6 +107,12 @@ SPARK_TOKEN=myaccesstoken
# CoreLightningWallet
CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
# CoreLightningRestWallet
CORELIGHTNING_REST_URL=http://127.0.0.1:8185/
# Path or BASE64/HEX STRING
CORELIGHTNING_REST_MACAROON="/path/to/clnrest/access.macaroon"
CORELIGHTNING_REST_CERT="/path/to/clnrest/tls.cert"
# LnbitsWallet
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY
@@ -101,10 +139,6 @@ LNPAY_API_KEY=LNPAY_API_KEY
# Wallet Admin in Wallet Access Keys
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
# LntxbotWallet
LNTXBOT_API_ENDPOINT=https://lntxbot.com/
LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY
@@ -121,8 +155,3 @@ ECLAIR_PASS=eclairpw
# Enter /api in LightningTipBot to get your key
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"
+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.5.1"
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
+53
View File
@@ -0,0 +1,53 @@
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
# docker:
# uses: ./.github/workflows/docker.yml
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 }}
+3 -3
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'
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 2
- run: git checkout HEAD^2
+20
View File
@@ -0,0 +1,20 @@
name: docker
on:
workflow_call:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: false
tags: lnbitsdocker/lnbits-legend:latest
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend:latest
cache-to: type=inline
-38
View File
@@ -1,38 +0,0 @@
name: formatting
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
checks:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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 packages
run: |
poetry config virtualenvs.create false
poetry install
- name: Check black
run: make checkblack
- name: Check isort
run: make checkisort
- uses: actions/setup-node@v3
- name: Check prettier
run: |
npm install prettier
make checkprettier
+27
View File
@@ -0,0 +1,27 @@
name: lint
on:
workflow_call:
jobs:
black:
uses: ./.github/workflows/make.yml
with:
make: checkblack
ruff:
uses: ./.github/workflows/make.yml
with:
make: checkruff
mypy:
uses: ./.github/workflows/make.yml
with:
make: mypy
pyright:
uses: ./.github/workflows/make.yml
with:
make: pyright
npm: true
prettier:
uses: ./.github/workflows/make.yml
with:
make: checkprettier
npm: true
+31
View File
@@ -0,0 +1,31 @@
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
jobs:
make:
name: ${{ inputs.make }} (${{ matrix.python-version }})
strategy:
matrix:
os-version: ["ubuntu-latest"]
python-version: ["3.9", "3.10"]
node-version: ["18.x"]
runs-on: ${{ matrix.os-version }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/prepare
with:
python-version: ${{ matrix.python-version }}
node-version: ${{ matrix.node-version }}
npm: ${{ inputs.npm }}
- run: make ${{ inputs.make }}
-44
View File
@@ -1,44 +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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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 config virtualenvs.create false
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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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 config virtualenvs.create false
poetry install
- name: Run tests
run: poetry run mypy
+28
View File
@@ -0,0 +1,28 @@
name: LNbits CI / nix
on:
push:
branches:
- main
- dev
paths:
- 'flake.nix'
- 'flake.lock'
- 'pyproject.toml'
- 'poetry.lock'
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'pyproject.toml'
- 'poetry.lock'
jobs:
nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v23
with:
nix_path: nixpkgs=channel:nixos-unstable
- run: nix build
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Import environment variables
id: import-env
@@ -66,4 +66,4 @@ jobs:
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \
--output "type=registry" ./
--output "type=registry" ./
+59 -114
View File
@@ -1,136 +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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- 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:
python-version: ${{ matrix.python-version }}
- name: Set up Poetry ${{ matrix.poetry-version }}
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
context: .
push: false
tags: lnbitsdocker/lnbits-legend:latest
cache-from: type=registry,ref=lnbitsdocker/lnbits-legend: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 config virtualenvs.create false
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-legend-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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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: 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 config virtualenvs.create false
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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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: 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 config virtualenvs.create false
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
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"
ECLAIR_URL: http://127.0.0.1:8082
ECLAIR_PASS: lnbits
run: make test-real-wallet
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
+36 -71
View File
@@ -1,93 +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:
venv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
run: make test-venv
sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
poetry-version: ["1.3.1"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
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 config virtualenvs.create false
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@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- uses: ./.github/actions/prepare
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 config virtualenvs.create false
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
+13 -3
View File
@@ -23,9 +23,9 @@ tests/data/*.sqlite3
*.pyc
*.env
.env
.venv
venv
.pre-commit-config.yaml
data
*.sqlite3
.pyre*
@@ -33,8 +33,18 @@ __bundle__
coverage.xml
node_modules
lnbits/static/bundle.*
lnbits/static/bundle.js
lnbits/static/bundle.css
docker
# Nix
*result*
# fly.io
fly.toml
lnbits-backup.zip
# Ignore extensions (post installable extension PR)
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: 23.7.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.283
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']
+13
View File
@@ -0,0 +1,13 @@
**/.git
**/.svn
**/.hg
**/node_modules
*.yml
**/lnbits/extensions/*
**/lnbits/upgrades/*
**/lnbits/static/vendor
**/lnbits/static/bundle.*
**/lnbits/static/css/*
+12 -6
View File
@@ -1,20 +1,26 @@
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 curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# needed for backups postgresql-client version 14 (pg_dump)
RUN apt-get install -y 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
RUN apt-get install -y postgresql-client-14
WORKDIR /app
RUN mkdir -p lnbits/data
COPY . .
RUN poetry config virtualenvs.create false
RUN poetry install --only main --no-root
RUN poetry run python build.py
RUN mkdir data
RUN poetry install --only main
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
+62 -40
View File
@@ -1,40 +1,42 @@
.PHONY: test
all: format check requirements.txt
all: format check
format: prettier isort black
format: prettier black ruff
check: mypy checkprettier checkisort checkblack
check: mypy pyright checkblack checkruff checkprettier
prettier: $(shell find lnbits -name "*.js" -o -name ".html")
./node_modules/.bin/prettier --write lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
prettier:
poetry run ./node_modules/.bin/prettier --write lnbits
pyright:
./node_modules/.bin/pyright
black:
poetry run black .
poetry run ./node_modules/.bin/pyright
mypy:
poetry run mypy
isort:
poetry run isort .
black:
poetry run black .
checkprettier: $(shell find lnbits -name "*.js" -o -name ".html")
./node_modules/.bin/prettier --check lnbits/static/js/*.js lnbits/core/static/js/*.js lnbits/extensions/*/templates/*/*.html ./lnbits/core/templates/core/*.html lnbits/templates/*.html lnbits/extensions/*/static/js/*.js lnbits/extensions/*/static/components/*/*.js lnbits/extensions/*/static/components/*/*.html
ruff:
poetry run ruff check . --fix
checkruff:
poetry run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check lnbits
checkblack:
poetry run black --check .
checkisort:
poetry run isort --check-only .
checkeditorconfig:
editorconfig-checker
dev:
poetry run lnbits --reload
test:
BOLTZ_NETWORK="regtest" \
BOLTZ_URL="http://127.0.0.1:9001" \
BOLTZ_MEMPOOL_SPACE_URL="http://127.0.0.1:8080" \
BOLTZ_MEMPOOL_SPACE_URL_WS="ws://127.0.0.1:8080" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
@@ -43,45 +45,65 @@ test:
poetry run pytest
test-real-wallet:
BOLTZ_NETWORK="regtest" \
BOLTZ_URL="http://127.0.0.1:9001" \
BOLTZ_MEMPOOL_SPACE_URL="http://127.0.0.1:8080" \
BOLTZ_MEMPOOL_SPACE_URL_WS="ws://127.0.0.1:8080" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest
test-venv:
BOLTZ_NETWORK="regtest" \
BOLTZ_URL="http://127.0.0.1:9001" \
BOLTZ_MEMPOOL_SPACE_URL="http://127.0.0.1:8080" \
BOLTZ_MEMPOOL_SPACE_URL_WS="ws://127.0.0.1:8080" \
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-migration:
rm -rf ./migration-data
mkdir -p ./migration-data
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
#
sass:
npm run sass
bundle:
npm install
npm run sass
npm run vendor_copy
npm run vendor_json
poetry run ./node_modules/.bin/prettier -w ./lnbits/static/vendor.json
npm run vendor_bundle_css
npm run vendor_minify_css
npm run vendor_bundle_js
npm run vendor_minify_js
# increment serviceworker version
sed -i -e "s/CACHE_VERSION =.*/CACHE_VERSION = $$(awk '/CACHE_VERSION =/ { print 1+$$4 }' lnbits/core/static/js/service-worker.js)/" \
lnbits/core/static/js/service-worker.js
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
pre-commit:
poetry run pre-commit run --all-files
+13 -16
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.9 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,15 @@ Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits serve
LNbits is a Python server that sits on top of any funding source. It can be used as:
* Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
* Extendable platform for exploring Lightning network functionality via the LNbits extension framework
* Part of a development stack via LNbits API
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
- Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
- Extendable platform for exploring Lightning network functionality via the LNbits extension framework
- Part of a development stack via LNbits API
- Fallback wallet for the LNURL scheme
- Instant wallet for LN demonstrations
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, lntxbot, LightningTipBot, and with more being added regularly.
LNbits can run on top of any Lightning funding source. It supports LND, CLN, Eclair, Spark, LNpay, OpenNode, LightningTipBot, and with more being added regularly.
See [docs.lnbits.org](https://docs.lnbits.org) for more detailed documentation.
See [LNbits Wiki](https://github.com/lnbits/lnbits/wiki/) for more detailed documentation.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
@@ -33,7 +31,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits
See the [install guide](docs/guide/installation.md) 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 +52,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,8 +66,7 @@ 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://docs.lnbits.org/
[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
[github-mypy-badge]: https://github.com/lnbits/lnbits/workflows/mypy/badge.svg
-105
View File
@@ -1,105 +0,0 @@
import glob
import os
import subprocess
import warnings
from os import path
from pathlib import Path
from typing import Any, List, NamedTuple, Optional
LNBITS_PATH = path.dirname(path.realpath(__file__)) + "/lnbits"
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".css", prefer_minified)
def sorter(key: str):
if "quasar@" in key:
return 1
if "vue@" in key:
return 2
if "chart.js@" in key:
return 100
return 9
return sorted(paths, key=sorter)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(
os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True
):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return sorted(paths)
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH)
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
(get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
def build():
transpile_scss()
bundle_vendored()
if __name__ == "__main__":
build()
+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:
+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
=====
+22 -10
View File
@@ -6,18 +6,32 @@ nav_order: 2
---
Making extensions
Extension set up
=================
Start off by copying the example extension in `lnbits/extensions/example` into your own:
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
cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use dashes or anything; it doesn't like those.
cd lnbits/extensions/mysuperplugin
cd [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'.
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.
@@ -28,18 +42,16 @@ Going over the example extension's structure:
Adding new dependencies
-----------------------
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
DO NOT ADD NEW DEPENDENCIES. Try to use the dependencies that are availabe in `pyproject.toml`. Getting the LNbits project to accept a new dependency is time consuming and uncertain, and may result in your extension NOT being made available to others.
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `venv`, or `poerty`:
If for some reason your extensions must have a new python package to work, and its nees are not met in `pyproject.toml`, you can add a new package using `poerty`:
```sh
$ poetry add <package>
# or
$ ./venv/bin/pip install <package>
```
**But we need an extra step to make sure LNbits doesn't break in production.**
Dependencies need to be added to `pyproject.toml` and `requirements.txt`, then tested by running on `venv` and `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
Dependencies need to be added to `pyproject.toml`, then tested by running on `poetry` compatability can be tested with `nix build .#checks.x86_64-linux.vmTest`.
SQLite to PostgreSQL migration
+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")`.
+16 -12
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 enviroment 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,14 +32,17 @@ There is also the possibility of posting the super user via webhook to another s
Admin Users
===========
enviroment variable: LNBITS_ADMIN_USERS, comma-seperated list of user ids
Admin Users can change settings in the admin ui aswell, 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
=============
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
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 .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`
+139
View File
@@ -0,0 +1,139 @@
# Extension Install
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
Extensions can be installed by an admin user after the **LNbits** instance has been started.
## Configure Repositories
Go to `Manage Server` > `Server` > `Extensions Manifests`
![image](https://user-images.githubusercontent.com/2951406/213494038-e8152d8e-61f2-4cb7-8b5f-361fc3f9a31f.png)
An `Extension Manifest` is a link to a `JSON` file which contains information about various extensions that can be installed (repository of extensions).
Multiple repositories can be configured. For more information check the [Manifest File](https://github.com/lnbits/lnbits/blob/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).
> **Warning**
> Extensions can have bugs or malicious code, be careful what you install!!
## Install New Extension
Only administrator users can install or upgrade extensions.
Go to `Manage Extensions` > `Add Remove Extensions`
![image](https://user-images.githubusercontent.com/2951406/213647560-67da4f8a-3315-436f-b690-3b3de536d2e6.png)
A list of extensions that can be installed is displayed:
![image](https://user-images.githubusercontent.com/2951406/213647904-d463775e-86b6-4354-a199-d50e08565092.png)
> **Note**
> If the extension is installed from a GitHub repo, then the GitHub star count will be shown.
Click the `Manage` button in order to install a particular release of the extension.
![image](https://user-images.githubusercontent.com/2951406/213648543-6c5c8cae-3bf4-447f-8499-344cac61c566.png)
> **Note**
> An extension can be listed in more than one repository. The admin user must select which repository it wants to install from.
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
The extension has been installed but it cannot be accessed yet. In order to activate the extension toggle it in the `Activated` state.
Go to `Manage Extensions` (as admin user or regular user). Search for the extension and enable it.
## Uninstall Extension
On the `Install` page click `Manage` for the extension you want to uninstall:
![image](https://user-images.githubusercontent.com/2951406/213653194-32cbb1da-dcc8-43cf-8a82-1ec5d2d3dc16.png)
The installed release is highlighted in green.
Click the `Uninstall` button for the release or the one in the bottom.
Users will no longer be able to access the extension.
> **Note**
> The database for the extension is not removed. If the extension is re-installed later, the data will be accessible.
## Manifest File
The manifest file is just a `JSON` file that lists a collection of extensions that can be installed. This file is of the form:
```json
{
"extensions": [...]
"repos": [...]
}
```
There are two ways to specify installable extensions:
### Explicit Release
It goes under the `extensions` object and it is of the form:
```json
{
"id": "lnurlp",
"name": "LNURL Pay Links",
"version": 1,
"shortDescription": "Upgrade to version 111111111",
"icon": "receipt",
"details": "All charge names should be <code>111111111</code>. API panel must show: <br>",
"archive": "https://github.com/lnbits/lnbits-extensions/raw/main/new/lnurlp/1/lnurlp.zip",
"hash": "a22d02de6bf306a7a504cd344e032cc6d48837a1d4aeb569a55a57507bf9a43a",
"htmlUrl": "https://github.com/lnbits/lnbits-extensions/tree/main/new/lnurlp/1",
"infoNotification": "This is a very old version",
"dependencies": ["other-ext-id"]
}
```
<details><summary>Fields Detailed Description</summary>
| Field | Type | | Description |
|----------------------|---------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | string | mandatory | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
| name | string | mandatory | User friendly name for the extension. It will be displayed on the installation page. |
| version | string | mandatory | Version of this release. [Semantic versioning](https://semver.org/) is recommended. |
| shortDescription | string | optional | A few words about the extension. It will be displayed on the installation page. |
| icon | string | optional | quasar valid icon name |
| details | string (html) | optional | Details about this particular release |
| archive | string | mandatory | URL to the `zip` file that contains the extension source-code |
| hash | string | mandatory | The hash (`sha256`) of the `zip` file. The extension will not be installed if the hash is incorrect. |
| 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.
</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:
```json
{
"id": "withdraw",
"organisation": "lnbits",
"repository": "withdraw-extension"
}
```
| Field | Type | Description |
|--------------|--------|-------------------------------------------------------|
| id | string | The ID of the extension. Must be unique for each extension. It is also used as the path in the URL. |
| organisation | string | The GitHub organisation (eg: `lnbits`) |
| repository | string | The GitHub repository name (eg: `withdraw-extension`) |
The admin user will see all releases from the Github repository:
![image](https://user-images.githubusercontent.com/2951406/213508934-11de5ae5-2045-471c-854b-94b6acbf4434.png)
+37 -37
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 importan 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>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>
</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/lnbits/tree/main/lnbits/extensions/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="http://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)
@@ -112,17 +112,17 @@ allow-self-payment=1
<ul>
<li>
<details><summary>A - Via an external swap service</summary>
<p>If the user do not have full acceess 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>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
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 regularily 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)
@@ -157,10 +157,10 @@ allow-self-payment=1
<details><summary>How can I use a LNbits lndhub account in other wallet apps?</summary>
<p>Open your LNbits with the account / wallet you want to use, go to "manage extensions" and activate the LNDHUB extension.</p>
<p>Open your LNbits with the account / wallet you want to use, go to "manage extensions" and activate the <a href="https://github.com/lnbits/lndhub">LNDHUB extension</a>.</p>
<p>Then open the LNDHUB extension, choose the wallet you want to use and scan the QR code you want to use: "admin" or "invoice only", depending on the security level you want for that wallet.</p>
<p>You can use <a href="https://zeusln.app">Zeus</a> or <a href="https://bluewallet.io">Bluewallet</a> as wallet apps for a lndhub account.</p>
<p>Keep in mind: if your LNbits instance is Tor only, you must use also theose apps behind Tor and open the LNbits page through your Tor .onion address.</p>
<p>Keep in mind: if your LNbits instance is Tor only, you must use also those apps behind Tor and open the LNbits page through your Tor .onion address.</p>
</details>
</ul>
</ul>
@@ -169,11 +169,11 @@ allow-self-payment=1
<ul> <p>LNbits has all sorts of open APIs and tools to program and connect to a lot of different devices for a gazillion of use-cases. Let us know what you did with it ! Come to the <a href="https://t.me/makerbits">Makerbits Telegram Group</a> if you are interested in building or if you need help with a project - we got you!</p>
<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. It works based on BTCPayer server, LNTXBOT is not longer an option. 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>
<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 👇 Ressources. It allows
<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>
<li>LNURLPoS Offline Lightning Payments. Passive interaction, sharing a secret as evidence</li>
<li>OnChain For onchain payments. Generates an address and displays a link for verification</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 👇 Ressources.</p>
<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>
@@ -209,7 +209,7 @@ allow-self-payment=1
<ul>
<li><a href="https://t.me/makerbits'">MakerBits</a> - Telegram support group</li>
<li><a href="https://ereignishorizont.xyz/">Instructions for LNpos, Switch, ATM, BTCticker</a> - guides in DE & EN</li>
<li><a href="https://shop.lnbits.com/">LNbits shop</a> - eadymade hardware gimmicks from the community</li>
<li><a href="https://shop.lnbits.com/">LNbits shop</a> - readymade hardware gimmicks from the community</li>
<li><a href="https://github.com/cryptoteun/awesome-lnbits#hardware-projects-utilizing-lnbits">Collection of hardware projects using LNbits</a></li>
</ul>
</details>
@@ -222,7 +222,7 @@ allow-self-payment=1
</details>
<details><summary>Swapping ⚡️LN BTC to a BTC address</summary>
<p>LNbits has two swap extensions integrated: <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltz">Boltz</a> and <a href="https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/deezy">Deezy</a>.</p>
<p>LNbits has two swap extensions integrated: <a href="https://github.com/lnbits/boltz-extension/">Boltz</a> and <a href="hhttps://github.com/lnbits/deezy">Deezy</a>.</p>
<p>For a merchant that uses LNbits to receive BTC payments through LN, this is very handy to move the received sats from LN channels into onchain wallets. It not only helps you HODLing but is also freeing up "space in your channels" so you are ready to receive more sats.</p>
<p>Boltz has an option to setup an automated swap triggered by a certain amount received.</p>
</details>
@@ -262,7 +262,7 @@ allow-self-payment=1
</ul>
## Developing for LNbits
<ul>
<ul>
<li><a href="http://docs.lnbits.org/devs/development.html">Making Estension / 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
+95 -56
View File
@@ -6,16 +6,17 @@ nav_order: 2
# Basic installation
You can choose between four package managers, `poetry`, `nix` and `venv`.
You can choose between four package managers, `poetry` and `nix`
By default, LNbits will use SQLite as its database. You can also use PostgreSQL which is recommended for applications with a high load (see guide below).
## Option 1 (recommended): poetry
If you have problems installing LNbits using these instructions, please have a look at the [Troubleshooting](#troubleshooting) section.
Mininum poetry version has is ^1.2, but it is recommended to use latest poetry. (including OSX)
```sh
git clone https://github.com/lnbits/lnbits.git
git checkout main
cd lnbits
# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version
@@ -26,8 +27,8 @@ sudo apt install python3.9 python3.9-distutils
curl -sSL https://install.python-poetry.org | python3 -
# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command
export PATH="/home/user/.local/bin:$PATH"
# Next command, you can exchange with python3.10 or newer versions.
export PATH="/home/user/.local/bin:$PATH"
# Next command, you can exchange with python3.10 or newer versions.
# Identify your version with python3 --version and specify in the next line
# command is only needed when your default python is not ^3.9 or ^3.10
poetry env use python3.9
@@ -36,7 +37,7 @@ poetry install --only main
mkdir data
cp .env.example .env
# set funding source amongst other options
nano .env
nano .env
```
#### Running the server
@@ -45,7 +46,7 @@ nano .env
poetry run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
# Note that you have to add the line DEBUG=true in your .env file, too.
# Note that you have to add the line DEBUG=true in your .env file, too.
```
#### Updating the server
@@ -58,7 +59,7 @@ poetry install --only main
# Start LNbits with `poetry run lnbits`
```
## Option 2: Nix
## Option 2: Nix
> note: currently not supported while we make some architectural changes on the path to leave beta
@@ -80,47 +81,33 @@ mkdir data
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
## Option 3: venv
## Option 3: Docker
use latest version from docker hub
```sh
docker pull lnbitsdocker/lnbits-legend
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
```
build the image yourself
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3.9-venv'
python3.9 -m venv venv
# If you have problems here, try `sudo apt install -y pkg-config libpq-dev`
./venv/bin/pip install -r requirements.txt
# create the data folder and the .env file
mkdir data && cp .env.example .env
# build the static files
./venv/bin/python build.py
```
#### Running the server
```sh
./venv/bin/uvicorn lnbits.__main__:app --port 5000
```
If you want to host LNbits on the internet, run with the option `--host 0.0.0.0`.
## Option 4: Docker
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits .
docker build -t lnbitsdocker/lnbits-legend .
cp .env.example .env
mkdir data
docker run --detach --publish 5000:5000 --name lnbits-legend --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits-legend
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
```
## Option 5: Fly.io
## Option 4: Fly.io
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
After install is complete, the command will output a command you should copy/paste/run to get `fly` into your `$PATH`. Something like:
@@ -145,7 +132,7 @@ fly launch
You'll be prompted to enter an app name, region, postgres (choose no), deploy now (choose no).
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
Note: Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`. Environment variable strings should be quoted here, so if in `.env` you have `LNBITS_ENDPOINT=https://legend.lnbits.com` in `fly.toml` you should have `LNBITS_ENDPOINT="https://legend.lnbits.com"`.
@@ -166,10 +153,10 @@ kill_timeout = 30
[env]
HOST="127.0.0.1"
PORT=5000
LNBITS_FORCE_HTTPS=true
FORWARDED_ALLOW_IPS="*"
LNBITS_BASEURL="https://mylnbits.lnbits.org/"
LNBITS_DATA_FOLDER="/data"
${PUT_YOUR_LNBITS_ENV_VARS_HERE}
...
@@ -202,15 +189,8 @@ sudo apt install python3.9-dev gcc build-essential
# if the secp256k1 build fails:
# if you used poetry
poetry add setuptools wheel
# if you used venv
./venv/bin/pip install setuptools wheel
```
#### Poetry
If your Poetry version is older than 1.2, for `poetry install`, ignore the `--only main` flag.
### Optional: PostgreSQL database
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
@@ -295,7 +275,7 @@ Description=LNbits
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits-legend
WorkingDirectory=/home/lnbits/lnbits
# same here. run `which poetry` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/poetry run lnbits
# replace with the user that you're running lnbits on
@@ -315,7 +295,8 @@ Save the file and run the following commands:
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## Reverse proxy with automatic https using Caddy
## Reverse proxy with automatic HTTPS using Caddy
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
@@ -327,11 +308,15 @@ https://caddyserver.com/docs/install#debian-ubuntu-raspbian
```
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Assuming your LNbits is running on port `5000` add:
```
yourdomain.com {
handle /api/v1/payments/sse* {
@@ -348,22 +333,30 @@ yourdomain.com {
}
}
```
Save and exit `CTRL + x`
```
sudo caddy start
```
## Running behind an apache2 reverse proxy over https
Install apache2 and enable apache2 mods
## Running behind an Apache2 reverse proxy over HTTPS
Install Apache2 and enable Apache2 mods:
```sh
apt-get install apache2 certbot
a2enmod headers ssl proxy proxy-http
```
create a ssl certificate with letsencrypt
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --webroot --agree-tos --text --non-interactive --webroot-path /var/www/html -d lnbits.org
certbot certonly --webroot --agree-tos --non-interactive --webroot-path /var/www/html -d lnbits.org
```
create a apache2 vhost at: /etc/apache2/sites-enabled/lnbits.conf
Create an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
```sh
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
<VirtualHost *:443>
@@ -388,16 +381,62 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
</VirtualHost>
EOF
```
restart apache2
Restart Apache2:
```sh
service restart apache2
```
## Running behind an Nginx reverse proxy over HTTPS
Install nginx:
```sh
apt-get install nginx certbot
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --nginx --agree-tos -d lnbits.org
```
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
```sh
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
server {
server_name lnbits.org;
location / {
proxy_pass http://127.0.0.1:5000;
}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/lnbits.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lnbits.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
EOF
```
Restart nginx:
```sh
service restart nginx
```
## Using https without reverse proxy
The most common way of using LNbits via https is to use a reverse proxy such as Caddy, nginx, or ngriok. However, you can also run LNbits via https without additional software. This is useful for development purposes or if you want to use LNbits in your local network.
We have to create a self-signed certificate using `mkcert`. Note that this certiciate is not "trusted" by most browsers but that's fine (since you know that you have created it) and encryption is always better than clear text.
We have to create a self-signed certificate using `mkcert`. Note that this certificate is not "trusted" by most browsers but that's fine (since you know that you have created it) and encryption is always better than clear text.
#### Install mkcert
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
@@ -425,7 +464,7 @@ mkcert localhost 127.0.0.1 ::1
You can then pass the certificate files to uvicorn when you start LNbits:
```sh
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
```
+12 -13
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, lntxbot and OpenNode, with more being added regularly.
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.
A backend wallet can be configured using the following LNbits environment variables:
@@ -18,7 +18,13 @@ 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 +35,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,13 +47,13 @@ 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
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
To encrypt your macaroon, run `poetry run python lnbits/wallets/macaroon/macaroon.py`.
### LNbits
@@ -57,7 +63,7 @@ To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroo
### LNPay
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
@@ -65,13 +71,6 @@ For the invoice listener to work you have a publicly accessible URL in your LNbi
- `LNPAY_WALLET_KEY`: waka_apiKey
### lntxbot
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
- `LNTXBOT_API_ENDPOINT`: https://lntxbot.com/
- `LNTXBOT_KEY`: lntxbotAdminApiKey
### OpenNode
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
+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
+52 -25
View File
@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
@@ -15,13 +18,34 @@
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"lastModified": 1678470307,
"narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f",
"type": "github"
},
"original": {
@@ -31,32 +55,20 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1657261001,
"narHash": "sha256-sUZeuRYfhG59uD6xafM07bc7bAIkpcGq84Vj4B+cyms=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0be91cefefde5701f8fa957904618a13e3bb51d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"lastModified": 1695386222,
"narHash": "sha256-5lgnhCCGW0NH5+m5iTED8u6NSSM/dbH9LBPvX0x0XXg=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"rev": "093383b3d7fdd36846a7d84e128ca11865800538",
"type": "github"
},
"original": {
@@ -70,6 +82,21 @@
"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"
}
}
},
"root": "root",
+24 -3
View File
@@ -1,7 +1,12 @@
{
description = "LNbits, free and open-source Lightning wallet and accounts system";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
@@ -17,19 +22,35 @@
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 = ./.;
python = pkgs.python39;
meta.rev = self.dirtyRev or self.rev;
overrides = pkgs.poetry2nix.overrides.withDefaults (final: prev: {
ruff = prev.ruff.override { preferWheel = true; };
fastapi = prev.fastapi.overridePythonAttrs (old: {
postPatch = ''
substituteInPlace pyproject.toml \
--replace '"Framework :: Pydantic",' "" \
--replace '"Framework :: Pydantic :: 1",' ""
'';
});
bolt11 = prev.bolt11.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [
prev.poetry
];
});
});
};
});
nixosModules = {
+350 -85
View File
@@ -1,36 +1,55 @@
import asyncio
import glob
import importlib
import logging
import os
import shutil
import signal
import sys
import traceback
from hashlib import sha256
from http import HTTPStatus
from pathlib import Path
from typing import Callable, List
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.staticfiles import StaticFiles
from loguru import logger
from slowapi import Limiter
from slowapi.util import get_remote_address
from starlette.responses import JSONResponse
from lnbits.core.tasks import register_task_listeners
from lnbits.settings import get_wallet_class, set_wallet_class, settings
from lnbits.core.crud import get_installed_extensions
from lnbits.core.helpers import migrate_extension_database
from lnbits.core.services import websocketUpdater
from lnbits.core.tasks import ( # register_watchdog,; unregister_watchdog,
register_killswitch,
register_task_listeners,
)
from lnbits.settings import settings
from lnbits.tasks import cancel_all_tasks, create_permanent_task
from lnbits.utils.cache import cache
from lnbits.wallets import get_wallet_class, set_wallet_class
from .commands import migrate_databases
from .core import core_app
from .core.services import check_admin_settings
from .core.views.generic import core_html_routes
from .helpers import (
get_css_vendored,
get_js_vendored,
get_valid_extensions,
template_renderer,
url_for_vendored,
from .commands import db_versions, load_disabled_extension_list, 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
from .helpers import template_renderer
from .middleware import (
CustomGZipMiddleware,
ExtensionsRedirectMiddleware,
InstalledExtensionMiddleware,
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,
@@ -39,18 +58,24 @@ from .tasks import (
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",
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
},
)
# 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))
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
app.mount(
"/core/static",
@@ -64,107 +89,335 @@ 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"]
)
# order of these two middlewares is important
app.add_middleware(InstalledExtensionMiddleware)
app.add_middleware(ExtensionsRedirectMiddleware)
register_custom_extensions_path()
# adds security middleware
add_ip_block_middleware(app)
add_ratelimit_middleware(app)
register_startup(app)
register_assets(app)
register_routes(app)
register_async_tasks(app)
register_exception_handlers(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(f"SIGINT received, terminating LNbits.")
logger.debug(
f"SIGINT received, terminating LNbits. signal: {signal}, frame: {frame}"
)
sys.exit(1)
signal.signal(signal.SIGINT, signal_handler)
WALLET = get_wallet_class()
# fallback to void after 30 seconds of failures
sleep_time = 5
timeout = int(30 / sleep_time)
balance = 0
retry_counter = 0
while True:
try:
error_message, balance = await WALLET.status()
if not error_message:
retry_counter = 0
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
logger.info("Retrying connection to backend in 5 seconds...")
await asyncio.sleep(5)
if settings.lnbits_admin_ui and retry_counter == timeout:
set_void_wallet_class()
WALLET = get_wallet_class()
break
else:
logger.warning(f"Retrying connection to backend in {sleep_time} seconds...")
retry_counter += 1
await asyncio.sleep(sleep_time)
signal.signal(signal.SIGINT, original_sigint_handler)
logger.info(
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
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.
"""
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
await load_disabled_extension_list()
installed_extensions = await build_all_installed_extensions_list()
for ext in installed_extensions:
try:
installed = check_installed_extension_files(ext)
if not installed:
await restore_installed_extension(app, ext)
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} ({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]:
"""
Returns a list of all the installed extensions plus the extensions that
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
"""
installed_extensions = await get_installed_extensions()
installed_extensions_ids = [e.id for e in installed_extensions]
for ext_id in settings.lnbits_extensions_default_install:
if ext_id in installed_extensions_ids:
continue
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
release = ext_releases[0] if len(ext_releases) else None
if release:
ext_info = InstallableExtension(
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
)
installed_extensions.append(ext_info)
return installed_extensions
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, "zips", "*.zip"))
if f"./{str(ext.zip_path)}" not in zip_files:
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)
await migrate_extension_database(extension, current_version)
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
if extension.upgrade_hash:
ext.nofiy_upgrade()
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():
try:
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
ext_route = getattr(ext_module, f"{ext.code}_ext")
if hasattr(ext_module, f"{ext.code}_start"):
ext_start_func = getattr(ext_module, f"{ext.code}_start")
ext_start_func()
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"])
logger.trace(f"adding route for extension {ext_module}")
app.include_router(ext_route)
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.
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)
ext_route = getattr(ext_module, f"{ext.code}_ext")
if hasattr(ext_module, f"{ext.code}_start"):
ext_start_func = getattr(ext_module, f"{ext.code}_start")
ext_start_func()
if hasattr(ext_module, f"{ext.code}_static_files"):
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
for s in ext_statics:
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")
settings.lnbits_extensions_redirects = [
r for r in settings.lnbits_extensions_redirects if r["ext_id"] != ext.code
]
for r in ext_redirects:
r["ext_id"] = ext.code
settings.lnbits_extensions_redirects.append(r)
logger.trace(f"adding route for extension {ext_module}")
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash != "" else ""
app.include_router(router=ext_route, prefix=prefix)
def register_startup(app: FastAPI):
@app.on_event("startup")
async def lnbits_startup():
try:
# 1. wait till migration is done
# wait till migration is done
await migrate_databases()
# 2. setup admin settings
# setup admin settings
await check_admin_settings()
await check_webpush_settings()
log_server_info()
# 3. initialize WALLET
set_wallet_class()
# initialize WALLET
try:
set_wallet_class()
except Exception as e:
logger.error(
f"Error initializing {settings.lnbits_backend_wallet_class}: "
f"{str(e)}"
)
set_void_wallet_class()
# 4. initialize funding source
# initialize funding source
await check_funding_source()
# check extensions after restart
await check_installed_extensions(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}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
logger.info(f"Port: {settings.port}")
logger.info(f"Debug: {settings.debug}")
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}")
@@ -174,25 +427,14 @@ 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_assets(app: FastAPI):
"""Serve each vendored asset separately or a bundle."""
@app.on_event("startup")
async def vendored_assets_variable():
if settings.debug:
g().VENDORED_JS = map(url_for_vendored, get_js_vendored())
g().VENDORED_CSS = map(url_for_vendored, get_css_vendored())
else:
g().VENDORED_JS = ["/static/bundle.js"]
g().VENDORED_CSS = ["/static/bundle.css"]
def register_async_tasks(app):
@app.route("/wallet/webhook")
async def webhook_listener():
@@ -200,17 +442,14 @@ def register_async_tasks(app):
@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()
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
register_task_listeners()
register_killswitch()
# await run_deferred_async() # calle: doesn't do anyting?
@app.on_event("shutdown")
async def stop_listeners():
pass
def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception)
@@ -286,20 +525,46 @@ 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
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)
+7 -370
View File
@@ -1,370 +1,7 @@
import hashlib
import re
import time
from decimal import Decimal
from typing import List, NamedTuple, Optional
import bitstring
import embit
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(object):
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
invoice.date = data.read(35).uint
while data.pos != data.len:
tag, tagdata, data = _pull_tagged(data)
data_length = len(tagdata) / 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
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(),
short_channel_id=_readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe,
)
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(
"Cannot encode {}: too many decimal places".format(addr.amount)
)
amount = addr.currency + shorten_amount(amount)
else:
amount = addr.currency if addr.currency else ""
hrp = "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("Duplicate '{}' tag".format(k))
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":
data += encode_fallback(v, addr.currency)
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("Unknown tag {}".format(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 not "d" in tags_set and not "h" 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(object):
def __init__(
self, paymenthash=None, amount=None, currency="bc", tags=None, date=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.currency = currency
self.amount = amount
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
bytes.hex(self.pubkey.serialize()).decode(),
self.amount,
self.currency,
", ".join([k + "=" + str(v) for k, v in self.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", ""]
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("Invalid amount '{}'".format(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)
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray
def tagged(char, l):
# Tagged fields need to be zero-padded to 5 bits.
while l.len % 5 != 0:
l.append("0b0")
return (
bitstring.pack(
"uint:5, uint:5, uint:5",
CHARSET.find(char),
(l.len / 5) / 32,
(l.len / 5) % 32,
)
+ l
)
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
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:
return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
outputindex=(short_channel_id & 0xFFFF),
)
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)
return ret
from bolt11 import (
Bolt11 as Invoice, # noqa: F401
)
from bolt11 import (
decode, # noqa: F401
encode, # noqa: F401
)
+94 -87
View File
@@ -1,8 +1,5 @@
import asyncio
import importlib
import os
import re
import warnings
from pathlib import Path
import click
from loguru import logger
@@ -11,112 +8,122 @@ 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.helpers import migrate_extension_database, run_migration
from .db import COCKROACH, POSTGRES, SQLITE
from .helpers import (
get_css_vendored,
get_js_vendored,
get_valid_extensions,
url_for_vendored,
)
from .extension_manager import get_valid_extensions
@click.command("migrate")
def db_migrate():
@click.group()
def lnbits_cli():
"""
Python CLI for LNbits
"""
@lnbits_cli.group()
def db():
"""
Database related commands
"""
def get_super_user() -> str:
"""Get the superuser"""
with open(Path(settings.lnbits_data_folder) / ".super_user", "r") as file:
return file.readline()
@lnbits_cli.command("superuser")
def superuser():
"""Prints the superuser"""
click.echo(get_super_user())
@lnbits_cli.command("superuser-url")
def superuser_url():
"""Prints the superuser"""
click.echo(f"http://{settings.host}:{settings.port}/wallet?usr={get_super_user()}")
@lnbits_cli.command("delete-settings")
def delete_settings():
"""Deletes the settings"""
async def wrap():
async with core_db.connect() as conn:
await conn.execute("DELETE from settings")
loop = asyncio.get_event_loop()
loop.run_until_complete(wrap())
@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()
bundle_vendored()
def transpile_scss():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from scss.compiler import compile_string # type: ignore
with open(os.path.join(settings.lnbits_path, "static/scss/base.scss")) as scss:
with open(
os.path.join(settings.lnbits_path, "static/css/base.css"), "w"
) as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(settings.lnbits_path, "static/bundle.js")),
(get_css_vendored, os.path.join(settings.lnbits_path, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
async def set_migration_version(conn, db_name, version):
await conn.execute(
"""
INSERT INTO dbversions (db, version) VALUES (?, ?)
ON CONFLICT (db) DO UPDATE SET version = ?
""",
(db_name, version, version),
)
async def run_migration(db, migrations_module, db_name):
for key, migrate in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
logger.debug(f"running migration {db_name}.{version}")
await migrate(db)
if db.schema == None:
await set_migration_version(db, db_name, version)
else:
async with core_db.connect() as conn:
await set_migration_version(conn, db_name, version)
async with core_db.connect() as conn:
exists = False
if conn.type == SQLITE:
exists = await conn.fetchone(
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
)
elif conn.type in {POSTGRES, COCKROACH}:
exists = await conn.fetchone(
"SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'"
"SELECT * FROM information_schema.tables WHERE table_schema = 'public'"
" AND table_name = 'dbversions'"
)
if not exists:
await core_migrations.m000_create_migrations_table(conn)
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_")
db_name = core_migrations.__name__.split(".")[-2]
await run_migration(conn, core_migrations, db_name)
current_versions = await get_dbversions(conn)
core_version = current_versions.get("core", 0)
await run_migration(conn, core_migrations, "core", core_version)
for ext in get_valid_extensions():
current_version = current_versions.get(ext.code, 0)
try:
module_str = (
ext.migration_module or f"lnbits.extensions.{ext.code}.migrations"
)
ext_migrations = importlib.import_module(module_str)
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
db_name = ext.db_name or module_str.split(".")[-2]
except ImportError:
raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations, db_name)
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")
def database_versions():
"""Show current database versions"""
loop = asyncio.get_event_loop()
loop.run_until_complete(db_versions())
async def db_versions():
"""Show current database versions"""
async with core_db.connect() as conn:
return await get_dbversions(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
def main():
"""main function"""
lnbits_cli()
if __name__ == "__main__":
main()
+20 -8
View File
@@ -1,12 +1,24 @@
from fastapi.routing import APIRouter
from fastapi import APIRouter
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
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
core_app: APIRouter = APIRouter()
# backwards compatibility for extensions
core_app = APIRouter(tags=["Core"])
from .views.admin_api import * # noqa
from .views.api import * # noqa
from .views.generic import * # noqa
from .views.public_api import * # noqa
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)
+473 -100
View File
@@ -1,24 +1,47 @@
import datetime
import json
from typing import Any, Dict, List, Optional
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 lnbits import bolt11
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.settings import AdminSettings, EditableSettings, SuperSettings, settings
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,
WebPushSettings,
settings,
)
from . import db
from .models import BalanceCheck, Payment, TinyURL, User, Wallet
from .models import (
BalanceCheck,
Payment,
PaymentFilters,
PaymentHistoryPoint,
TinyURL,
User,
Wallet,
WebPushSubscription,
)
# accounts
# --------
async def create_account(conn: Optional[Connection] = None) -> User:
user_id = uuid4().hex
async def create_account(
conn: Optional[Connection] = None, user_id: Optional[str] = None
) -> User:
if user_id:
user_uuid4 = UUID(hex=user_id, version=4)
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
else:
user_id = uuid4().hex
await (conn or db).execute("INSERT INTO accounts (id) VALUES (?)", (user_id,))
new_account = await get_account(user_id=user_id, conn=conn)
@@ -49,9 +72,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,),
)
@@ -61,13 +86,128 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
return User(
id=user["id"],
email=user["email"],
extensions=[e[0] for e in extensions],
extensions=[
e[0] for e in extensions if User.is_extension_for_user(e[0], user["id"])
],
wallets=[Wallet(**w) for w in wallets],
admin=user["id"] == settings.super_user
or user["id"] in settings.lnbits_admin_users,
super_user=user["id"] == settings.super_user,
)
# extensions
# -------
async def add_installed_extension(
ext: InstallableExtension,
conn: Optional[Connection] = None,
) -> None:
meta = {
"installed_release": (
dict(ext.installed_release) if ext.installed_release else None
),
"dependencies": ext.dependencies,
}
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) =
(?, ?, ?, ?, ?, ?, ?)
""",
(
ext.id,
version,
ext.name,
ext.short_description,
ext.icon,
ext.stars,
json.dumps(meta),
version,
ext.name,
False,
ext.short_description,
ext.icon,
ext.stars,
json.dumps(meta),
),
)
async def update_installed_extension_state(
*, ext_id: str, active: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
UPDATE installed_extensions SET active = ? WHERE id = ?
""",
(active, ext_id),
)
async def delete_installed_extension(
*, ext_id: str, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
DELETE from installed_extensions WHERE id = ?
""",
(ext_id,),
)
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):
row = await (conn or db).fetchone(
"SELECT * FROM installed_extensions WHERE id = ?",
(ext_id,),
)
return dict(row) if row else None
async def get_installed_extensions(
conn: Optional[Connection] = None,
) -> List["InstallableExtension"]:
rows = await (conn or db).fetchall(
"SELECT * FROM installed_extensions",
(),
)
return [InstallableExtension.from_row(row) for row in rows]
async def get_inactive_extensions(*, conn: Optional[Connection] = None) -> List[str]:
inactive_extensions = await (conn or db).fetchall(
"""SELECT id FROM installed_extensions WHERE NOT active""",
(),
)
return [ext[0] for ext in inactive_extensions]
async def update_user_extension(
*, user_id: str, extension: str, active: bool, conn: Optional[Connection] = None
) -> None:
@@ -112,16 +252,29 @@ 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]:
return await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
WHERE id = ?
set_clause = []
values = []
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(
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"
return wallet
async def delete_wallet(
@@ -129,11 +282,8 @@ async def delete_wallet(
) -> None:
await (conn or db).execute(
"""
UPDATE wallets AS w
SET
"user" = 'del:' || w."user",
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
UPDATE wallets
SET deleted = true
WHERE id = ? AND "user" = ?
""",
(wallet_id, user_id),
@@ -145,9 +295,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,),
)
@@ -156,13 +305,14 @@ 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 = ?
""",
(key, key),
)
@@ -170,7 +320,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)
@@ -181,6 +331,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
# ---------------
@@ -232,7 +387,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}
@@ -246,7 +401,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,
@@ -255,29 +410,23 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
) -> Page[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
args: List[Any] = []
values: List[Any] = []
clause: List[str] = []
if since != 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)
if since is not None:
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
@@ -301,30 +450,53 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
return await (conn or db).fetch_page(
"SELECT * FROM apipayments",
clause,
values,
filters=filters,
model=Payment,
)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
rows = await (conn or db).fetchall(
f"""
SELECT *
FROM apipayments
{where}
ORDER BY time DESC
{limit_offset_clause}
""",
tuple(args),
async def get_payments(
*,
wallet_id: Optional[str] = None,
complete: bool = False,
pending: bool = False,
outgoing: bool = False,
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
filters: Optional[Filters[PaymentFilters]] = None,
conn: Optional[Connection] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
) -> list[Payment]:
"""
Filters payments to be returned by complete | pending | outgoing | incoming.
"""
filters = filters or Filters()
filters.sortby = filters.sortby or "time"
filters.direction = filters.direction or "desc"
filters.limit = limit or filters.limit
filters.offset = offset or filters.offset
page = await get_payments_paginated(
wallet_id=wallet_id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
since=since,
exclude_uncheckable=exclude_uncheckable,
filters=filters,
conn=conn,
)
return [Payment.from_row(row) for row in rows]
return page.data
async def delete_expired_invoices(
@@ -362,22 +534,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(
"""
@@ -396,11 +562,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,
),
)
@@ -427,7 +595,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] = []
@@ -461,7 +628,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"
@@ -481,10 +649,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(
@@ -499,6 +734,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
@@ -512,6 +751,26 @@ async def check_internal(
return row["checking_id"]
async def check_internal_pending(
payment_hash: str, conn: Optional[Connection] = None
) -> bool:
"""
Returns False if the internal payment is not pending anymore
(and thus paid), otherwise True
"""
row = await (conn or db).fetchone(
"""
SELECT pending FROM apipayments
WHERE hash = ? AND amount > 0
""",
(payment_hash,),
)
if not row:
return True
else:
return row["pending"]
# balance_check
# -------------
@@ -598,30 +857,65 @@ async def get_admin_settings(is_super_user: bool = False) -> Optional[AdminSetti
row_dict = dict(sets)
row_dict.pop("super_user")
admin_settings = AdminSettings(
super_user=is_super_user,
is_super_user=is_super_user,
lnbits_allowed_funding_sources=settings.lnbits_allowed_funding_sources,
**row_dict,
)
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(f"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):
async def update_super_user(super_user: str) -> SuperSettings:
await db.execute("UPDATE settings SET super_user = ?", (super_user,))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "updated super_user settings could not be retrieved"
return settings
async def create_admin_settings(super_user: str, new_settings: dict):
sql = f"INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
sql = "INSERT INTO settings (super_user, editable_settings) VALUES (?, ?)"
await db.execute(sql, (super_user, json.dumps(new_settings)))
return await get_super_settings()
settings = await get_super_settings()
assert settings, "created admin settings could not be retrieved"
return settings
# db versions
# --------------
async def get_dbversions(conn: Optional[Connection] = None):
rows = await (conn or db).fetchall("SELECT * FROM dbversions")
return {row["db"]: row["version"] for row in rows}
async def update_migration_version(conn, db_name, version):
await (conn or db).execute(
"""
INSERT INTO dbversions (db, version) VALUES (?, ?)
ON CONFLICT (db) DO UPDATE SET version = ?
""",
(db_name, version, 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
@@ -631,7 +925,7 @@ async def create_admin_settings(super_user: str, new_settings: dict):
async def create_tinyurl(domain: str, endless: bool, wallet: str):
tinyurl_id = shortuuid.uuid()[:8]
await db.execute(
f"INSERT INTO tiny_url (id, url, endless, wallet) VALUES (?, ?, ?, ?)",
"INSERT INTO tiny_url (id, url, endless, wallet) VALUES (?, ?, ?, ?)",
(
tinyurl_id,
domain,
@@ -644,7 +938,7 @@ async def create_tinyurl(domain: str, endless: bool, wallet: str):
async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
row = await db.fetchone(
f"SELECT * FROM tiny_url WHERE id = ?",
"SELECT * FROM tiny_url WHERE id = ?",
(tinyurl_id,),
)
return TinyURL.from_row(row) if row else None
@@ -652,14 +946,93 @@ async def get_tinyurl(tinyurl_id: str) -> Optional[TinyURL]:
async def get_tinyurl_by_url(url: str) -> List[TinyURL]:
rows = await db.fetchall(
f"SELECT * FROM tiny_url WHERE url = ?",
"SELECT * FROM tiny_url WHERE url = ?",
(url,),
)
return [TinyURL.from_row(row) for row in rows]
async def delete_tinyurl(tinyurl_id: str):
row = await db.execute(
f"DELETE FROM tiny_url WHERE id = ?",
await db.execute(
"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()
+79
View File
@@ -0,0 +1,79 @@
import importlib
import re
from typing import Any
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 .crud import update_migration_version
async def migrate_extension_database(ext: Extension, current_version):
try:
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
ext_db = importlib.import_module(ext.module_name).db
except ImportError as e:
logger.error(e)
raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations, ext.code, current_version)
async def run_migration(
db: Connection, migrations_module: Any, db_name: str, current_version: int
):
matcher = re.compile(r"^m(\d\d\d)_")
for key, migrate in migrations_module.__dict__.items():
match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_version:
logger.debug(f"running migration {db_name}.{version}")
print(f"running migration {db_name}.{version}")
await migrate(db)
if db.schema is None:
await update_migration_version(db, db_name, version)
else:
async with core_db.connect() as conn:
await update_migration_version(conn, db_name, version)
async def stop_extension_background_work(ext_id: str, user: str):
"""
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 it fails it tries using `https`.
"""
async with httpx.AsyncClient() as client:
try:
url = f"http://{settings.host}:{settings.port}/{ext_id}/api/v1?usr={user}"
await client.delete(url)
except Exception as ex:
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)
+114 -4
View File
@@ -107,7 +107,7 @@ async def m002_add_fields_to_apipayments(db):
continue
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = "#" + ext + " "
prefix = f"#{ext} "
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
await db.execute(
@@ -168,7 +168,8 @@ async def m004_ensure_fees_are_always_negative(db):
async def m005_balance_check_balance_notify(db):
"""
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
"""
await db.execute(
@@ -238,7 +239,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(
"""
@@ -250,7 +252,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
@@ -283,3 +285,111 @@ async def m009_create_tinyurl_table(db):
);
"""
)
async def m010_create_installed_extensions_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS installed_extensions (
id TEXT PRIMARY KEY,
version TEXT NOT NULL,
name TEXT NOT NULL,
short_description TEXT,
icon TEXT,
stars INT NOT NULL DEFAULT 0,
active BOOLEAN DEFAULT false,
meta TEXT NOT NULL DEFAULT '{}'
);
"""
)
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")
);
"""
)
+139 -11
View File
@@ -3,8 +3,10 @@ import hashlib
import hmac
import json
import time
from dataclasses import dataclass
from enum import Enum
from sqlite3 import Row
from typing import Dict, List, Optional
from typing import Callable, Dict, List, Optional
from ecdsa import SECP256k1, SigningKey
from fastapi import Query
@@ -12,9 +14,10 @@ from lnurl import encode as lnurl_encode
from loguru import logger
from pydantic import BaseModel
from lnbits.db import Connection
from lnbits.db import Connection, FilterModel, FromRowModel
from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class
from lnbits.settings import settings
from lnbits.wallets import get_wallet_class
from lnbits.wallets.base import PaymentStatus
@@ -24,7 +27,9 @@ class Wallet(BaseModel):
user: str
adminkey: str
inkey: str
currency: Optional[str]
balance_msat: int
deleted: bool
@property
def balance(self) -> int:
@@ -38,11 +43,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:
@@ -59,6 +63,22 @@ 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 User(BaseModel):
id: str
email: Optional[str] = None
@@ -76,8 +96,18 @@ class User(BaseModel):
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
@classmethod
def is_extension_for_user(cls, ext: str, user: str) -> bool:
if ext not in settings.lnbits_admin_extensions:
return True
if user == settings.super_user:
return True
if user in settings.lnbits_admin_users:
return True
return False
class Payment(BaseModel):
class Payment(FromRowModel):
checking_id: str
pending: bool
amount: int
@@ -160,6 +190,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(
@@ -170,7 +202,8 @@ class Payment(BaseModel):
return PaymentStatus(None)
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()
@@ -184,7 +217,8 @@ 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)
elif self.is_out and status.failed:
@@ -194,15 +228,41 @@ class Payment(BaseModel):
await self.delete(conn)
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):
@@ -215,6 +275,11 @@ class BalanceCheck(BaseModel):
return cls(wallet=row["wallet"], service=row["service"], url=row["url"])
class CoreAppExtra:
register_new_ext_routes: Callable
register_new_ratelimiter: Callable
class TinyURL(BaseModel):
id: str
url: str
@@ -225,3 +290,66 @@ 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
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
+255 -87
View File
@@ -1,52 +1,58 @@
import asyncio
import datetime
import json
from io import BytesIO
from typing import Dict, List, Optional, Tuple
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, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.helpers import url_for
from lnbits.settings import (
FAKE_WALLET,
EditableSettings,
get_wallet_class,
SuperSettings,
readonly_variables,
send_admin_user_to_saas,
settings,
)
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 PaymentResponse, PaymentStatus
from . import db
from .crud import (
check_internal,
check_internal_pending,
create_account,
create_admin_settings,
create_payment,
create_wallet,
delete_wallet_payment,
get_account,
get_standalone_payment,
get_super_settings,
get_total_balance,
get_wallet,
get_wallet_payment,
update_admin_settings,
update_payment_details,
update_payment_status,
update_super_user,
)
from .models import Payment
try:
from typing import TypedDict
except ImportError: # pragma: nocover
from typing_extensions import TypedDict
from .helpers import to_valid_user_id
from .models import Payment, Wallet
class PaymentFailure(Exception):
@@ -57,41 +63,93 @@ 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,
expiry: Optional[int] = None,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
internal: Optional[bool] = False,
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
if not amount > 0:
raise InvoiceFailure("Amountless invoices not supported.")
if await get_wallet(wallet_id, conn=conn) is None:
raise InvoiceFailure("Wallet does not exist.")
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
)
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,
@@ -112,16 +170,23 @@ 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)
invoice = bolt11_decode(payment_request)
if not invoice.amount_msat or not 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.")
fee_reserve_msat = fee_reserve(invoice.amount_msat)
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
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}"
@@ -130,6 +195,10 @@ async def pay_invoice(
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):
wallet_id: str
@@ -137,6 +206,7 @@ async def pay_invoice(
payment_hash: str
amount: int
memo: str
expiry: Optional[datetime.datetime]
extra: Optional[Dict]
payment_kwargs: PaymentKwargs = PaymentKwargs(
@@ -144,16 +214,35 @@ 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)
if not await check_internal_pending(invoice.payment_hash, conn=conn):
raise PaymentFailure("Internal invoice already paid.")
# check_internal() returns the checking_id of the invoice we're waiting for
# (pending only)
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id:
# perform additional checks on the internal payment
# the payment hash is not enough to make sure that this is the same invoice
internal_invoice = await get_standalone_payment(
internal_checking_id, incoming=True, conn=conn
)
assert internal_invoice is not None
if (
internal_invoice.amount != invoice.amount_msat
or internal_invoice.bolt11 != payment_request.lower()
):
raise PaymentFailure("Invalid invoice.")
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet
await create_payment(
new_payment = await create_payment(
checking_id=internal_id,
fee=0,
pending=False,
@@ -164,21 +253,27 @@ async def pay_invoice(
logger.debug(f"creating temporary payment with id {temp_id}")
# create a temporary payment here so we can check if
# the balance is enough in the next step
await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
try:
new_payment = await create_payment(
checking_id=temp_id,
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
except Exception as e:
logger.error(f"could not create temporary payment: {e}")
# happens if the same wallet tries to pay an invoice twice
raise PaymentFailure("Could not make payment.")
# do the balance check
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
assert wallet, "Wallet for balancecheck could not be fetched"
if wallet.balance_msat < 0:
logger.debug("balance is too low, deleting temporary payment")
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
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_msat/1000)} sat) to"
" cover potential routing fees."
)
raise PermissionError("Insufficient balance.")
@@ -191,6 +286,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
@@ -207,26 +303,33 @@ 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}")
if payment.checking_id and payment.ok != False:
if payment.checking_id and payment.ok is not False:
# payment.ok can be True (paid) or None (pending)!
logger.debug(f"updating payment {temp_id}")
async with db.connect() as conn:
await update_payment_details(
checking_id=temp_id,
pending=payment.ok != True,
pending=payment.ok is not True,
fee=payment.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 == False:
elif payment.checking_id is None and payment.ok is False:
# payment failed
logger.warning(f"backend sent payment failure")
logger.warning("backend sent payment failure")
async with db.connect() as conn:
logger.debug(f"deleting temporary payment {temp_id}")
await delete_wallet_payment(temp_id, wallet_id, conn=conn)
@@ -236,7 +339,8 @@ 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}"
)
return invoice.payment_hash
@@ -268,9 +372,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
@@ -321,19 +426,20 @@ async def perform_lnurlauth(
return b
def encode_strict_der(r_int, s_int, order):
def encode_strict_der(r: int, s: int, order: int):
# if s > order/2 verification will fail sometimes
# so we must fix it here (see https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147)
if s_int > order // 2:
s_int = order - s_int
# so we must fix it here see:
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
if s > order // 2:
s = order - s
# now we do the strict DER encoding copied from
# https://github.com/KiriKiri/bip66 (without any checks)
r = int_to_bytes_suitable_der(r_int)
s = int_to_bytes_suitable_der(s_int)
r_temp = int_to_bytes_suitable_der(r)
s_temp = int_to_bytes_suitable_der(s)
r_len = len(r)
s_len = len(s)
r_len = len(r_temp)
s_len = len(s_temp)
sign_len = 6 + r_len + s_len
signature = BytesIO()
@@ -341,16 +447,17 @@ async def perform_lnurlauth(
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(r_temp)
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
signature.write(s_temp)
return signature.getvalue()
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
async with httpx.AsyncClient() as client:
assert key.verifying_key, "LNURLauth verifying_key does not exist"
r = await client.get(
callback,
params={
@@ -381,45 +488,60 @@ async def check_transaction_status(
return PaymentStatus(None)
if not payment.pending:
# note: before, we still checked the status of the payment again
return PaymentStatus(True)
return PaymentStatus(True, fee_msat=payment.fee)
status: PaymentStatus = await payment.check_status()
return status
# 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
# 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:
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))
async def update_wallet_balance(wallet_id: str, amount: int):
internal_id = f"internal_{urlsafe_short_hash()}"
payment = await create_payment(
wallet_id=wallet_id,
checking_id=internal_id,
payment_request="admin_internal",
payment_hash="admin_internal",
amount=amount * 1000,
memo="Admin top up",
pending=False,
async def send_payment_notification(wallet: Wallet, payment: Payment):
await websocketUpdater(
wallet.id,
json.dumps(
{
"wallet_balance": wallet.balance,
"payment": payment.dict(),
}
),
)
# manually send this for now
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(internal_id)
return payment
async def update_wallet_balance(wallet_id: str, amount: int):
payment_hash, _ = await create_invoice(
wallet_id=wallet_id,
amount=amount,
memo="Admin top up",
internal=True,
)
async with db.connect() as conn:
checking_id = await check_internal(payment_hash, conn=conn)
assert checking_id, "newly created checking_id cannot be retrieved"
await update_payment_status(checking_id=checking_id, pending=False, conn=conn)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(checking_id)
async def check_admin_settings():
if settings.super_user:
settings.super_user = to_valid_user_id(settings.super_user).hex
if settings.lnbits_admin_ui:
settings_db = await get_super_settings()
if not settings_db:
# 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
@@ -427,15 +549,9 @@ async def check_admin_settings():
update_cached_settings(settings_db.dict())
# printing settings for debugging
logger.debug(f"Admin settings:")
for key, value in settings.dict(exclude_none=True).items():
logger.debug(f"{key}: {value}")
admin_url = (
f"http://{settings.host}:{settings.port}/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 (
@@ -445,25 +561,52 @@ async def check_admin_settings():
):
send_admin_user_to_saas()
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 not key in readonly_variables:
if key not in readonly_variables:
try:
setattr(settings, key, value)
except:
logger.error(f"error overriding setting: {key}, value: {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"])
async def init_admin_settings(super_user: str = None):
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
account = None
if super_user:
account = await get_account(super_user)
if not account:
account = await create_account()
super_user = account.id
account = await create_account(user_id=super_user)
if not account.wallets or len(account.wallets) == 0:
await create_wallet(user_id=account.id)
@@ -473,12 +616,12 @@ async def init_admin_settings(super_user: str = None):
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):
@@ -495,3 +638,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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

-36
View File
@@ -1,36 +0,0 @@
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
filteredExtensions: 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)
)
}
}
},
mixins: [windowMixin]
})
+13 -2
View File
@@ -5,14 +5,22 @@ new Vue({
return {
disclaimerDialog: {
show: false,
data: {}
data: {},
description: ''
},
walletName: ''
}
},
computed: {
formatDescription() {
return LNbits.utils.convertMarkdown(this.description)
}
},
methods: {
createWallet: function () {
LNbits.href.createWallet(this.walletName)
LNbits.api.createAccount(this.walletName).then(res => {
window.location = '/wallet?usr=' + res.data.user + '&wal=' + res.data.id
})
},
processing: function () {
this.$q.notify({
@@ -21,5 +29,8 @@ new Vue({
icon: null
})
}
},
created() {
this.description = SITE_DESCRIPTION
}
})
+268
View File
@@ -0,0 +1,268 @@
function shortenNodeId(nodeId) {
return nodeId
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
: '...'
}
Vue.component('lnbits-node-ranks', {
props: ['ranks'],
data: function () {
return {
user: {},
stats: [
{label: 'Capacity', key: 'capacity'},
{label: 'Channels', key: 'channelcount'},
{label: 'Age', key: 'age'},
{label: 'Growth', key: 'growth'},
{label: 'Availability', key: 'availability'}
]
}
},
template: `
<q-card class='q-my-none'>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>1ml Node Rank</h5>
<div v-for='stat in stats' class='q-gutter-sm'>
<div class='row items-center'>
<div class='col-9'>{{ stat.label }}</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (ranks && ranks[stat.key]) ?? '-' }}
</div>
</div>
</div>
</div>
</q-card>
`
})
Vue.component('lnbits-channel-stats', {
props: ['stats'],
data: function () {
return {
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'}
]
}
},
template: `
<q-card>
<div class='column q-ma-md'>
<h5 class='text-subtitle1 text-bold q-my-none'>Channels</h5>
<div v-for='state in states' class='q-gutter-sm'>
<div class='row'>
<div class='col-9'>
<q-badge rounded size='md' :color='state.color'>{{ state.label }}</q-badge>
</div>
<div class='col-3 text-subtitle1 text-bold'>
{{ (stats?.counts && stats.counts[state.value]) ?? "-" }}
</div>
</div>
</div>
</div>
</q-card>
`,
created: function () {
if (window.user) {
this.user = LNbits.map.user(window.user)
}
}
})
Vue.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value: function () {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})
Vue.component('lnbits-node-qrcode', {
props: ['info'],
mixins: [windowMixin],
template: `
<q-card class="my-card">
<q-card-section>
<div class="text-h6">
<div style="text-align: center">
<qrcode
:value="info.addresses[0]"
:options="{width: 250}"
v-if='info.addresses[0]'
class="rounded-borders"
></qrcode>
<div v-else class='text-subtitle1'>
No addresses available
</div>
</div>
</div>
</q-card-section>
<q-card-actions vertical>
<q-btn
dense
unelevated
size="md"
@click="copyText(info.id)"
>Public Key<q-tooltip> Click to copy </q-tooltip>
</q-btn>
</q-card-actions>
</q-card>
`
})
Vue.component('lnbits-node-info', {
props: ['info'],
data() {
return {
showDialog: false
}
},
mixins: [windowMixin],
methods: {
shortenNodeId
},
template: `
<div class='row items-baseline q-gutter-x-sm'>
<div class='text-h4 text-bold'>{{ this.info.alias }}</div>
<div class='row items-center q-gutter-sm'>
<div class='text-subtitle1 text-light'>{{ this.info.backend_name }}</div>
<q-badge
:style='\`background-color: #\${this.info.color}\`'
class='text-bold'
>
#{{ this.info.color }}
</q-badge>
<div class='text-bold'>{{ shortenNodeId(this.info.id) }}</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='showDialog = true'
></q-btn>
</div>
<q-dialog v-model="showDialog">
<lnbits-node-qrcode :info='info'></lnbits-node-qrcode>
</q-dialog>
</div>
`
})
Vue.component('lnbits-stat', {
props: ['title', 'amount', 'msat', 'btc'],
computed: {
value: function () {
return (
this.amount ??
(this.btc
? LNbits.utils.formatSat(this.btc)
: LNbits.utils.formatMsat(this.msat))
)
}
},
template: `
<q-card>
<q-card-section>
<div class='text-overline text-primary'>
{{ title }}
</div>
<div>
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
<span class='text-h5' v-if='msat != undefined'>sats</span>
<span class='text-h5' v-if='btc != undefined'>BTC</span>
</div>
</q-card-section>
</q-card>
`
})
Vue.component('lnbits-channel-balance', {
props: ['balance', 'color'],
methods: {
formatMsat: function (msat) {
return LNbits.utils.formatMsat(msat)
}
},
template: `
<div>
<div class="row items-center justify-between">
<span class="text-weight-thin">
Local: {{ formatMsat(balance.local_msat) }}
sats
</span>
<span class="text-weight-thin">
Remote: {{ formatMsat(balance.remote_msat) }}
sats
</span>
</div>
<q-linear-progress
rounded
size="25px"
:value="balance.local_msat / balance.total_msat"
:color="color"
:style="\`color: #\${this.color}\`"
>
<div class="absolute-full flex flex-center">
<q-badge
color="white"
text-color="accent"
:label="formatMsat(balance.total_msat) + ' sats'"
>
{{ balance.alias }}
</q-badge>
</div>
</q-linear-progress>
</div>
`
})
Vue.component('lnbits-date', {
props: ['ts'],
computed: {
date: function () {
return Quasar.utils.date.formatDate(
new Date(this.ts * 1000),
'YYYY-MM-DD HH:mm'
)
},
dateFrom: function () {
return moment(this.date).fromNow()
}
},
template: `
<div>
<q-tooltip>{{ this.date }}</q-tooltip>
{{ this.dateFrom }}
</div>
`
})
+32 -2
View File
@@ -1,5 +1,6 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
// update cache version every time there is a new deployment
// so the service worker reinitializes the cache
const CACHE_VERSION = 64
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {
@@ -55,3 +56,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))
})
+217 -112
View File
@@ -3,45 +3,24 @@
Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader)
function generateChart(canvas, payments) {
var txs = []
var n = 0
var data = {
labels: [],
income: [],
outcome: [],
cumulative: []
}
_.each(
payments.filter(p => !p.pending).sort((a, b) => a.time - b.time),
tx => {
txs.push({
hour: Quasar.utils.date.formatDate(tx.date, 'YYYY-MM-DDTHH:00'),
sat: tx.sat
})
function generateChart(canvas, rawData) {
const data = rawData.reduce(
(previous, current) => {
previous.labels.push(current.date)
previous.income.push(current.income)
previous.spending.push(current.spending)
previous.cumulative.push(current.balance)
return previous
},
{
labels: [],
income: [],
spending: [],
cumulative: []
}
)
_.each(_.groupBy(txs, 'hour'), (value, day) => {
var income = _.reduce(
value,
(memo, tx) => (tx.sat >= 0 ? memo + tx.sat : memo),
0
)
var outcome = _.reduce(
value,
(memo, tx) => (tx.sat < 0 ? memo + Math.abs(tx.sat) : memo),
0
)
n = n + income - outcome
data.labels.push(day)
data.income.push(income)
data.outcome.push(outcome)
data.cumulative.push(n)
})
new Chart(canvas.getContext('2d'), {
return new Chart(canvas.getContext('2d'), {
type: 'bar',
data: {
labels: data.labels,
@@ -64,7 +43,7 @@ function generateChart(canvas, payments) {
backgroundColor: window.Color('rgb(76,175,80)').alpha(0.5).rgbString() // green
},
{
data: data.outcome,
data: data.spending,
type: 'bar',
label: 'out',
barPercentage: 0.75,
@@ -85,7 +64,7 @@ function generateChart(canvas, payments) {
{
type: 'time',
display: true,
offset: true,
//offset: true,
time: {
minUnit: 'hour',
stepSize: 3
@@ -137,6 +116,9 @@ new Vue({
comment: ''
},
paymentChecker: null,
copy: {
show: false
},
camera: {
show: false,
camera: 'auto'
@@ -146,39 +128,113 @@ new Vue({
paymentsTable: {
columns: [
{
name: 'memo',
name: 'time',
align: 'left',
label: 'Memo',
field: 'memo'
},
{
name: 'date',
align: 'left',
label: 'Date',
label: this.$t('memo') + '/' + this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'sat',
name: 'amount',
align: 'right',
label: 'Amount (' + LNBITS_DENOMINATION + ')',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
}
],
pagination: {
rowsPerPage: 10,
page: 1,
sortBy: 'time',
descending: true,
rowsNumber: 10
},
filter: null,
loading: false
},
paymentsCSV: {
columns: [
{
name: 'pending',
align: 'left',
label: 'Pending',
field: 'pending'
},
{
name: 'memo',
align: 'left',
label: this.$t('memo'),
field: 'memo'
},
{
name: 'time',
align: 'left',
label: this.$t('date'),
field: 'date',
sortable: true
},
{
name: 'amount',
align: 'right',
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
},
{
name: 'fee',
align: 'right',
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
label: this.$t('fee') + ' (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
},
{
name: 'tag',
align: 'right',
label: this.$t('tag'),
field: 'tag'
},
{
name: 'payment_hash',
align: 'right',
label: this.$t('payment_hash'),
field: 'payment_hash'
},
{
name: 'payment_proof',
align: 'right',
label: this.$t('payment_proof'),
field: 'payment_proof'
},
{
name: 'webhook',
align: 'right',
label: this.$t('webhook'),
field: 'webhook'
},
{
name: 'fiat_currency',
align: 'right',
label: 'Fiat Currency',
field: row => row.extra.wallet_fiat_currency
},
{
name: 'fiat_amount',
align: 'right',
label: 'Fiat Amount',
field: row => row.extra.wallet_fiat_amount
}
],
pagination: {
rowsPerPage: 10
},
filter: null
filter: null,
loading: false
},
paymentsChart: {
show: false
show: false,
group: {value: 'hour', label: 'Hour'},
groupOptions: [
{value: 'month', label: 'Month'},
{value: 'day', label: 'Day'},
{value: 'hour', label: 'Hour'}
],
instance: null
},
disclaimerDialog: {
show: false,
@@ -186,7 +242,10 @@ new Vue({
},
balance: 0,
credit: 0,
newName: ''
update: {
name: null,
currency: null
}
}
},
computed: {
@@ -228,9 +287,27 @@ new Vue({
},
showChart: function () {
this.paymentsChart.show = true
this.$nextTick(() => {
generateChart(this.$refs.canvas, this.payments)
})
LNbits.api
.request(
'GET',
'/api/v1/payments/history?group=' + this.paymentsChart.group.value,
this.g.wallet.adminkey
)
.then(response => {
this.$nextTick(() => {
if (this.paymentsChart.instance) {
this.paymentsChart.instance.destroy()
}
this.paymentsChart.instance = generateChart(
this.$refs.canvas,
response.data
)
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
this.paymentsChart.show = false
})
},
focusInput(el) {
this.$nextTick(() => this.$refs[el].focus())
@@ -243,7 +320,6 @@ new Vue({
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.unit = 'sat'
this.receive.paymentChecker = null
this.receive.minMax = [0, 2100000000000000]
this.receive.lnurl = null
this.focusInput('setAmount')
@@ -253,10 +329,13 @@ new Vue({
this.parse.invoice = null
this.parse.lnurlpay = null
this.parse.lnurlauth = null
this.parse.copy.show =
window.isSecureContext && navigator.clipboard?.readText !== undefined
this.parse.data.request = ''
this.parse.data.comment = ''
this.parse.data.paymentChecker = null
this.parse.camera.show = false
this.focusInput('textArea')
},
updateBalance: function (credit) {
LNbits.api
@@ -266,7 +345,7 @@ new Vue({
this.g.user.wallets[0].adminkey,
{
amount: credit,
id: this.g.user.wallets[0].id
id: this.g.wallet.id
}
)
.then(response => {
@@ -285,11 +364,6 @@ new Vue({
LNbits.utils.notifyApiError(error)
})
},
closeReceiveDialog: function () {
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
}, 10000)
},
closeParseDialog: function () {
setTimeout(() => {
clearInterval(this.parse.paymentChecker)
@@ -302,7 +376,6 @@ new Vue({
if (this.receive.paymentHash === paymentHash) {
this.receive.show = false
this.receive.paymentHash = null
clearInterval(this.receive.paymentChecker)
}
},
createInvoice: function () {
@@ -347,19 +420,7 @@ new Vue({
}
}
clearInterval(this.receive.paymentChecker)
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
}, 40000)
this.receive.paymentChecker = setInterval(() => {
let hash = response.data.payment_hash
LNbits.api.getPayment(this.g.wallet, hash).then(response => {
if (response.data.paid) {
this.onPaymentReceived(hash)
}
})
}, 5000)
this.fetchPayments()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
@@ -466,6 +527,16 @@ new Vue({
return
}
// BIP-21 support
if (this.parse.data.request.toLowerCase().includes('lightning')) {
this.parse.data.request = this.parse.data.request.split('lightning=')[1]
// fail safe to check there's nothing after the lightning= part
if (this.parse.data.request.includes('&')) {
this.parse.data.request = this.parse.data.request.split('&')[0]
}
}
let invoice
try {
invoice = decode(this.parse.data.request)
@@ -510,7 +581,7 @@ new Vue({
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...'
message: this.$t('processing_payment')
})
LNbits.api
@@ -631,7 +702,7 @@ new Vue({
LNbits.api
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
.then(response => {
.then(_ => {
dismissAuthMsg()
this.$q.notify({
message: `Authentication successful.`,
@@ -654,47 +725,69 @@ new Vue({
}
})
},
updateWalletName: function () {
let newName = this.newName
let adminkey = this.g.wallet.adminkey
if (!newName || !newName.length) return
updateWallet: function (data) {
LNbits.api
.request('PUT', '/api/v1/wallet/' + newName, adminkey, {})
.then(res => {
this.newName = ''
.request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data)
.then(_ => {
this.$q.notify({
message: `Wallet named updated.`,
message: `Wallet updated.`,
type: 'positive',
timeout: 3500
})
LNbits.href.updateWallet(
res.data.name,
this.user.id,
this.g.wallet.id
)
window.location.reload()
})
.catch(err => {
this.newName = ''
LNbits.utils.notifyApiError(err)
})
},
deleteWallet: function (walletId, user) {
deleteWallet: function () {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
.onOk(() => {
LNbits.href.deleteWallet(walletId, user)
LNbits.api
.deleteWallet(this.g.wallet)
.then(_ => {
this.$q.notify({
timeout: 3000,
message: `Wallet deleted!`,
spinner: true
})
})
.catch(err => {
this.paymentsTable.loading = false
LNbits.utils.notifyApiError(err)
})
})
},
fetchPayments: function () {
return LNbits.api.getPayments(this.g.wallet).then(response => {
this.payments = response.data
.map(obj => {
fetchPayments: function (props) {
// Props are passed by qasar when pagination or sorting changes
if (props) {
this.paymentsTable.pagination = props.pagination
}
let pagination = this.paymentsTable.pagination
this.paymentsTable.loading = true
const query = {
limit: pagination.rowsPerPage,
offset: (pagination.page - 1) * pagination.rowsPerPage,
sortby: pagination.sortBy ?? 'time',
direction: pagination.descending ? 'desc' : 'asc'
}
if (this.paymentsTable.filter) {
query.search = this.paymentsTable.filter
}
return LNbits.api
.getPayments(this.g.wallet, query)
.then(response => {
this.paymentsTable.loading = false
this.paymentsTable.pagination.rowsNumber = response.data.total
this.payments = response.data.data.map(obj => {
return LNbits.map.payment(obj)
})
.sort((a, b) => {
return b.time - a.time
})
})
})
.catch(err => {
this.paymentsTable.loading = false
LNbits.utils.notifyApiError(err)
})
},
fetchBalance: function () {
LNbits.api.getWallet(this.g.wallet).then(response => {
@@ -709,25 +802,37 @@ new Vue({
// status is important for export but it is not in paymentsTable
// because it is manually added with payment detail link and icons
// and would cause duplication in the list
let columns = structuredClone(this.paymentsTable.columns)
columns.unshift({
name: 'pending',
align: 'left',
label: 'Pending',
field: 'pending'
LNbits.api.getPayments(this.g.wallet, {}).then(response => {
const payments = response.data.data.map(LNbits.map.payment)
LNbits.utils.exportCSV(
this.paymentsCSV.columns,
payments,
this.g.wallet.name + '-payments'
)
})
},
pasteToTextArea: function () {
this.$refs.textArea.focus() // Set cursor to textarea
navigator.clipboard.readText().then(text => {
this.$refs.textArea.value = text
})
LNbits.utils.exportCSV(columns, this.payments)
}
},
watch: {
payments: function () {
this.fetchBalance()
},
'paymentsChart.group': function () {
this.showChart()
}
},
created: function () {
this.fetchBalance()
this.fetchPayments()
this.update.name = this.g.wallet.name
this.update.currency = this.g.wallet.currency
LNbits.api
.request('GET', '/api/v1/currencies')
.then(response => {
+115 -13
View File
@@ -4,40 +4,115 @@ 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
from lnbits.core.crud import (
get_balance_notify,
get_wallet,
get_webpush_subscriptions_for_user,
)
from lnbits.core.db import db
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 (
SseListenersDict,
create_permanent_task,
create_task,
register_invoice_listener,
send_push_notification,
)
api_invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict(
"api_invoice_listeners"
)
async def register_task_listeners():
def register_killswitch():
"""
Registers an invoice listener queue for the core tasks.
Incoming payaments in this queue will eventually trigger the signals sent to all other extensions
Registers a killswitch which will check lnbits-status repository for a signal from
LNbits and will switch to VoidWallet if the killswitch is triggered.
"""
logger.debug("Starting killswitch task")
create_permanent_task(killswitch_task)
async def killswitch_task():
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.ConnectError, httpx.RequestError):
logger.error(
"Cannot fetch lnbits status manifest."
f" {settings.lnbits_status_manifest}"
)
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
async def register_watchdog():
"""
Registers a watchdog which will check lnbits balance and nodebalance
and will switch to VoidWallet if the watchdog delta is reached.
"""
# TODO: implement watchdog properly
# logger.debug("Starting watchdog task")
# create_permanent_task(watchdog_task)
async def watchdog_task():
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)
def register_task_listeners():
"""
Registers an invoice listener queue for the core tasks. Incoming payments in this
queue will eventually trigger the signals sent to all other extensions
and fulfill other core tasks such as dispatching webhooks.
"""
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))
create_task(wait_for_paid_invoices(invoice_paid_queue))
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
"""
This worker dispatches events to all extensions, dispatches webhooks and balance notifys.
This worker dispatches events to all extensions,
dispatches webhooks and balance notifys.
"""
while True:
payment = await invoice_paid_queue.get()
logger.trace("received invoice paid event")
# send information to sse channel
await dispatch_api_invoice_listeners(payment)
wallet = await get_wallet(payment.wallet_id)
if wallet:
await send_payment_notification(wallet, payment)
# dispatch webhook
if payment.webhook and not payment.webhook_status:
await dispatch_webhook(payment)
@@ -52,6 +127,8 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
except (httpx.ConnectError, httpx.RequestError):
pass
await send_payment_push_notification(payment)
async def dispatch_api_invoice_listeners(payment: Payment):
"""
@@ -70,11 +147,15 @@ async def dispatch_webhook(payment: Payment):
"""
Dispatches the webhook to the webhook url.
"""
logger.debug("sending webhook", payment.webhook)
if not payment.webhook:
return await mark_webhook_sent(payment, -1)
async with httpx.AsyncClient() as client:
data = payment.dict()
try:
logger.debug("sending webhook", payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40) # type: ignore
r = await client.post(payment.webhook, json=data, timeout=40)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
@@ -88,3 +169,24 @@ async def mark_webhook_sent(payment: Payment, status: int) -> None:
""",
(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:
url = (
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
)
await send_push_notification(subscription, title, body, url)
+69 -68
View File
@@ -9,86 +9,87 @@
<ul>
{%raw%}
<li>Funding Source: {{settings.lnbits_backend_wallet_class}}</li>
<li>Balance: {{balance / 1000}} sats</li>
<li>
Node Balance: {{(auditData.node_balance_msats /
1000).toLocaleString()}} sats
</li>
<li>
LNbits Balance: {{(auditData.lnbits_balance_msats /
1000).toLocaleString()}} sats
</li>
<li>
Reserve Percent: {{(auditData.node_balance_msats /
auditData.lnbits_balance_msats * 100).toFixed(2)}} %
</li>
{%endraw%}
</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-6">
<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>
<div class="col-12 col-md-4">
<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-6">
<div class="col-12">
<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 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>
</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,264 @@
<q-tab-panel name="security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none" v-text="$t('security_tools')"></h6>
<br />
<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"
>{% raw %}{{ log }}{% endraw %}<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>
{%raw%}
<q-chip
v-for="blocked_ip in formData.lnbits_blocked_ips"
:key="blocked_ip"
removable
@remove="removeBlockedIPs(blocked_ip)"
color="primary"
text-color="white"
>
{{ blocked_ip }}
</q-chip>
{%endraw%}
</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>
{%raw%}
<q-chip
v-for="allowed_ip in formData.lnbits_allowed_ips"
:key="allowed_ip"
removable
@remove="removeAllowedIPs(allowed_ip)"
color="primary"
text-color="white"
>
{{ allowed_ip }}
</q-chip>
{%endraw%}
</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>
</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,70 @@
{% raw %}
<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"
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"
>{{ 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">
{{ formatDate(props.row.date) }}
</q-td>
<q-td key="message" :props="props"
>{{ props.row.message }}
<a
v-if="props.row.link"
target="_blank"
:href="props.row.link"
v-text="$t('more')"
></a
></q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
{% endraw %}
+70 -16
View File
@@ -33,22 +33,7 @@
<br />
</div>
<div class="col-12 col-md-6">
<p>Miscelaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Force HTTPS</q-item-label>
<q-item-label caption>Prefer secure URLs</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_force_https"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<p>Miscellaneous</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Hide API</q-item-label>
@@ -69,6 +54,75 @@
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>Allowed currencies</p>
<q-select
filled
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>
<div>
<p>Extension Sources</p>
<q-input
filled
v-model="formAddExtensionsManifest"
@keydown.enter="addExtensionsManifest"
type="text"
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
hint="Repositories from where the extensions can be downloaded"
>
<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"
removable
@remove="removeExtensionsManifest(manifestUrl)"
color="primary"
text-color="white"
>
{{ manifestUrl }}
</q-chip>
{%endraw%}
</div>
<br />
</div>
</div>
</q-card-section>
</q-tab-panel>
+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">
@@ -58,31 +58,5 @@
</div>
<br />
</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.name)"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p>Disabled Extensions</p>
<q-select
filled
v-model="formData.lnbits_disabled_extensions"
:options="g.extensions.map(e => e.name)"
multiple
hint="Disable extensions *amilk disabled by default as resource heavy"
label="Disable extensions"
></q-select>
<br />
</div>
</div>
</q-card-section>
</q-tab-panel>
+58 -419
View File
@@ -3,12 +3,15 @@
<div class="row q-col-gutter-md justify-center">
<div class="col q-my-md">
<q-btn
label="Save"
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges"> Save your changes </q-tooltip>
<q-tooltip v-if="checkChanges"
>{%raw%}{{ $t('save_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="checkChanges"
color="red"
@@ -17,15 +20,17 @@
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
label="Restart server"
:label="$t('restart')"
color="primary"
@click="restartServer"
>
<q-tooltip v-if="needsRestart">
Restart the server for changes to take effect
</q-tooltip>
<q-tooltip v-if="needsRestart"
>{%raw%}{{ $t('restart_tooltip') }}{%endraw%}</q-tooltip
>
<q-badge
v-if="needsRestart"
color="red"
@@ -34,31 +39,31 @@
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
label="Topup"
:label="$t('topup')"
color="primary"
@click="topUpDialog.show = true"
>
<q-tooltip> Add funds to a wallet. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('add_funds_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
<!-- <q-btn
label="Download Database Backup"
flat
@click="downloadBackup"
></q-btn> -->
<q-btn :label="$t('download_backup')" flat @click="downloadBackup"></q-btn>
<q-btn
flat
v-if="isSuperUser"
label="Reset to defaults"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip> Delete all settings and reset to defaults. </q-tooltip>
<q-tooltip>{%raw%}{{ $t('reset_defaults_tooltip') }}{%endraw%}</q-tooltip>
</q-btn>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
@@ -67,41 +72,54 @@
<q-tabs v-model="tab" active-color="primary" align="justify">
<q-tab
name="funding"
label="Funding"
:label="$t('funding')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="users"
label="Users"
:label="$t('users')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="server"
label="Server"
:label="$t('server')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="security"
:label="$t('security')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="theme"
label="Theme"
:label="$t('theme')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div>
</div>
<q-form name="settings_form" id="settings_form">
<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>
</div>
</div>
<q-dialog v-if="isSuperUser" v-model="topUpDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form class="q-gutter-md">
<p>TopUp a wallet</p>
<p v-text="$t('topup_wallet')"></p>
<div class="row">
<div class="col-12">
<q-input
@@ -110,420 +128,41 @@
filled
v-model="wallet.id"
label="Wallet ID"
hint="Use the wallet ID to topup any wallet"
:hint="$t('topup_hint')"
></q-input>
<br />
</div>
<div class="col-12">
<q-input
dense
type="number"
filled
v-model="wallet.amount"
label="Topup amount"
:label="$t('amount')"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn label="Topup" color="primary" @click="topupWallet"></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
<q-btn
:label="$t('topup')"
color="primary"
@click="topupWallet"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
</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',
'freedom',
'mint',
'autumn',
'monochrome',
'salvador'
],
formData: {},
formAddAdmin: '',
formAddUser: '',
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'
}
}
],
[
'CLightningWallet',
{
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'
},
lnd_cert: {
value: null,
label: 'Certificate'
},
lnd_rest_admin_macaroon: {
value: null,
label: 'Admin Macaroon'
},
lnd_rest_invoice_macaroon: {
value: null,
label: 'Invoice 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_invoice_macaroon: {
value: null,
label: 'Invoice Macaroon'
},
lnd_grpc_macaroon_encrypted: {
value: null,
label: 'Encrypted Macaroon'
}
}
],
[
'LntxbotWallet',
{
lntxbot_api_endpoint: {
value: null,
label: 'Endpoint'
},
lntxbot_key: {
value: null,
label: '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: 'Endpoint'
},
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'
}
}
],
[
'LnTipsWallet',
{
lntips_api_endpoint: {
value: null,
label: 'Endpoint'
},
lntips_api_key: {
value: null,
label: 'API Key'
}
}
]
])
}
},
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
)
},
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() {
LNbits.api
.request('GET', '/admin/api/v1/backup/?usr=' + this.g.user.id)
.then(response => {
this.$q.notify({
type: 'positive',
message:
'Success! Database backup request, download starts soon!',
icon: null
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}
}
})
</script>
<script src="/static/js/admin.js"></script>
{% endblock %}
+5 -6
View File
@@ -1,7 +1,7 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:label="$t('api_docs')"
:content-inset-level="0.5"
>
<q-card-section>
@@ -48,9 +48,9 @@
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;, "unit":
&lt;string&gt;, "webhook": &lt;url:string&gt;, "internal":
&lt;bool&gt;}</code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;,
"expiry": &lt;int&gt;, "unit": &lt;string&gt;, "webhook":
&lt;url:string&gt;, "internal": &lt;bool&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
@@ -62,8 +62,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key:
"amount": &lt;int&gt;, "memo": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
>
</q-card-section>
+665 -41
View File
@@ -1,46 +1,115 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script>
{% endblock %} {% block page %}
%} {{ window_vars(user, extensions) }}{% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-sm-3 col-xs-8 q-ml-auto">
<q-input v-model="searchTerm" label="Search extensions">
<div class="col-sm-9 col-xs-12">
<p class="text-h4 gt-sm">{%raw%}{{ $t('extensions') }}{%endraw%}</p>
</div>
<div class="col-sm-3 col-xs-12 q-ml-auto">
<q-input v-model="searchTerm" :label="$t('search_extensions')">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer q-mt-lg"
class="cursor-pointer"
/>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-xs">
<div class="q-gutter-y-md">
<q-tabs
v-model="tab"
@input="handleTabChanged"
active-color="primary"
align="left"
>
<q-tab
name="installed"
:label="$t('installed')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="all"
:label="$t('all')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="featured"
:label="$t('featured')"
@update="val => tab = val.name"
></q-tab>
<i v-if="!g.user.admin && tab != 'installed'"
>{%raw%}{{ $t('only_admins_can_install') }}{%endraw%}</i
>
</q-tabs>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
v-for="extension in filteredExtensions"
:key="extension.code"
:key="extension.id + extension.hash"
>
<q-card>
<q-card-section>
<q-card-section style="min-height: 140px" class="q-pb-none">
<div class="row">
<div class="col-3">
<q-img
:src="extension.tile"
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>{%raw%}{{ $t('new_version') }}{%endraw%}</small>
<q-tooltip
><span v-text="extension.latestRelease.version"></span
></q-tooltip>
</q-badge>
{% raw %}
<div class="text-h5 gt-sm q-mt-sm q-mb-xs">
<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 }}
{{ extension.shortDescription ||
extension.installedRelease?.description }}
</div>
<div class="text-subtitle1 lt-md q-mt-sm q-mb-xs">
{{ extension.name }}
@@ -54,55 +123,610 @@
{% endraw %}
</div>
</div>
<div class="row q-pt-sm">
<div class="col">
<small v-if="extension.dependencies?.length"
>{%raw%}{{ $t('extension_depends_on') }}{%endraw%}</small
>
<small v-else>&nbsp;</small>
<q-badge
v-for="dep in extension.dependencies"
:key="dep"
color="orange"
>
<small v-text="dep"></small>
</q-badge>
</div>
</div>
</q-card-section>
<q-card-section>
<div>
<q-rating
class="gt-sm"
v-model="maxStars"
disable
size="2em"
size="1.5em"
:max="5"
color="primary"
></q-rating
><q-rating
><q-tooltip
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
></q-rating
>
<q-rating
v-model="maxStars"
class="lt-md"
size="1.5em"
:max="5"
color="primary"
></q-rating
><q-tooltip>Ratings coming soon</q-tooltip>
><q-tooltip
>{%raw%}{{ $t('extension_rating_soon') }}{%endraw%}</q-tooltip
></q-rating
>
<q-toggle
v-if="extension.isAvailable && extension.isInstalled && g.user.admin"
:label="extension.isActive ? $t('activated'): $t('deactivated') "
color="secondary"
style="max-height: 21px"
v-model="extension.isActive"
@input="toggleExtension(extension)"
><q-tooltip
>{%raw%}{{ $t('activate_extension_details')
}}{%endraw%}</q-tooltip
></q-toggle
>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-actions>
<div v-if="extension.isEnabled">
<q-btn
flat
color="primary"
type="a"
:href="[extension.url, '?usr=', g.user.id].join('')"
>Open</q-btn
<q-card-actions style="min-height: 52px">
<div class="col-10">
<div v-if="!extension.inProgress">
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="primary"
type="a"
:href="[extension.id, '/?usr=', g.user.id].join('')"
>{%raw%}{{ $t('open') }}{%endraw%}</q-btn
>
<q-btn
v-if="user.extensions.includes(extension.id) && extension.isActive && extension.isInstalled"
flat
color="grey-5"
type="a"
:href="['{{
url_for('install.extensions')
}}', '?usr=', g.user.id, '&disable=', extension.id].join('')"
>
{%raw%}{{ $t('disable') }}{%endraw%}</q-btn
>
<q-badge v-if="extension.isAdminOnly && !user.admin">
{%raw%}{{ $t('admin_only') }}{%endraw%}
</q-badge>
<q-btn
v-else-if="extension.isInstalled && extension.isActive && !user.extensions.includes(extension.id)"
flat
color="primary"
type="a"
:href="['{{
url_for('install.extensions')
}}', '?usr=', g.user.id, '&enable=', extension.id].join('')"
>
{%raw%}{{ $t('enable') }}{%endraw%}
<q-tooltip>
<span v-text="$t('enable_extension_details')">
</span> </q-tooltip
></q-btn>
<q-btn
@click="showUpgrade(extension)"
flat
color="primary"
v-if="g.user.admin"
>
{%raw%}{{ $t('manage') }}{%endraw%}<q-tooltip
>{%raw%}{{ $t('manage_extension_details')
}}{%endraw%}</q-tooltip
></q-btn
>
</div>
<div v-else>
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
</div>
<div class="col-2">
<div
v-if="extension.isInstalled && extension.installedRelease"
class="float-right"
>
<q-btn
flat
color="grey-5"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&disable=', extension.code].join('')"
>
Disable</q-btn
>
<q-badge>
{% raw %}{{ extension.installedRelease.version }}{% endraw
%}<q-tooltip
>{%raw%}{{ $t('extension_installed_version')
}}{%endraw%}</q-tooltip
>
</q-badge>
</div>
</div>
<q-btn
v-else
flat
color="primary"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')"
>
Enable</q-btn
>
</q-card-actions>
</q-card>
</div>
<q-dialog v-model="showUninstallDialog">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
<p>
{%raw%}{{ $t('extension_uninstall_warning') }}{%endraw%} <br />
{%raw%}{{ $t('confirm_continue') }}{%endraw%}
</p>
<div class="row q-mt-lg">
<q-checkbox
v-model="uninstallAndDropDb"
value="false"
label="Cleanup database tables"
>
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left">
{%raw%}{{ $t('extension_db_drop_info') }}{%endraw%}
</q-tooltip>
</q-checkbox>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="uninstallExtension()"
>{%raw%}{{ $t('uninstall_confirm') }}{%endraw%}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showDropDbDialog">
<q-card v-if="selectedExtension" class="q-pa-lg">
<h6 class="q-my-md text-primary">{%raw%}{{ $t('warning') }}{%endraw%}</h6>
<p>{%raw%}{{ $t('extension_db_drop_warning') }}{%endraw%} <br /></p>
<q-input
v-model="dropDbExtensionId"
:label="selectedExtension.id"
></q-input>
<br />
<p>{%raw%}{{ $t('confirm_continue') }}{%endraw%}</p>
<div class="row q-mt-lg">
<q-btn
:disable="dropDbExtensionId !== selectedExtension.id"
outline
color="red"
@click="dropExtensionDb()"
>{%raw%}{{ $t('confirm') }}{%endraw%}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>{%raw%}{{ $t('cancel') }}{%endraw%}</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="showUpgradeDialog">
<q-card class="q-pa-lg lnbits__dialog-card">
<q-card-section>
<div class="text-h6" v-text="selectedExtension?.name"></div>
</q-card-section>
<div class="col-12 col-md-5 q-gutter-y-md" v-if="selectedExtensionRepos">
<q-card
flat
bordered
class="my-card"
v-for="repoName of Object.keys(selectedExtensionRepos)"
:key="repoName"
>
<q-expansion-item
:key="repoName"
group="repos"
:caption="repoName"
:content-inset-level="0.5"
:default-opened="selectedExtensionRepos[repoName].isInstalled"
>
<template v-slot:header>
<q-item-section avatar>
<q-avatar
:icon="selectedExtensionRepos[repoName].isInstalled ? 'download_done': 'download'"
:text-color="selectedExtensionRepos[repoName].isInstalled ? 'green' : ''"
/>
</q-item-section>
<q-item-section>
<div class="row">
<div class="col-10">
{%raw%}{{ $t('repository') }}{%endraw%}
<br />
<small v-text="repoName"></small>
<q-tooltip
><span
v-text="selectedExtensionRepos[repoName].repo"
></span
></q-tooltip>
</div>
<div class="col-2"></div>
</div>
</q-item-section>
</template>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
<q-expansion-item
v-for="release of selectedExtensionRepos[repoName].releases"
:key="release.version"
group="releases"
@click="getGitHubReleaseDetails(release)"
:icon="getReleaseIcon(release)"
:label="release.description"
:caption="release.version"
:content-inset-level="0.5"
:header-class="getReleaseIconColor(release)"
>
<div v-if="release.inProgress">
<q-spinner color="primary" size="2.55em"></q-spinner>
</div>
<div v-else-if="release.error">
<q-icon
class="gt-sm"
name="error"
color="pink"
size="70px"
></q-icon>
Cannot get the release details.
</div>
<q-card v-else>
<q-card-section v-if="release.is_version_compatible">
<q-btn
v-if="!release.isInstalled"
@click="installExtension(release)"
color="primary unelevated mt-lg pt-lg"
>{%raw%}{{ $t('install') }}{%endraw%}</q-btn
>
<q-btn v-else @click="showUninstall()" flat color="red">
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
>
<a
v-if="release.html_url"
class="text-secondary float-right"
:href="release.html_url"
target="_blank"
style="color: inherit"
>{%raw%}{{ $t('release_notes') }}{%endraw%}</a
>
</q-card-section>
<q-card-section v-else>
{%raw%}{{ $t('extension_min_lnbits_version') }}{%endraw%}
<strong>
<span v-text="release.min_lnbits_version"></span>
</strong>
</q-card-section>
<q-card v-if="release.warning">
<q-card-section>
<div class="text-h6">
<q-badge color="yellow" text-color="black">
{%raw%}{{ $t('warning') }}{%endraw%}
</q-badge>
</div>
<div class="text-subtitle2">
<span v-text="release.warning"></span>
</div>
</q-card-section>
</q-card>
<q-separator></q-separator> </q-card
></q-expansion-item>
</q-list>
</q-card-section>
</q-expansion-item>
</q-card>
</div>
<q-spinner v-else color="primary" size="2.55em"></q-spinner>
<div class="row q-mt-lg">
<q-btn
v-if="selectedExtension?.isInstalled"
@click="showUninstall()"
flat
color="red"
>
{%raw%}{{ $t('uninstall') }}{%endraw%}</q-btn
>
<q-btn
v-else-if="selectedExtension?.hasDatabaseTables"
@click="showDropDb()"
flat
color="red"
:label="$t('drop_db')"
></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
{%raw%}{{ $t('close') }}{%endraw%}</q-btn
>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
data: function () {
return {
searchTerm: '',
tab: 'all',
filteredExtensions: null,
showUninstallDialog: false,
showUpgradeDialog: false,
showDropDbDialog: false,
dropDbExtensionId: '',
selectedExtension: null,
selectedExtensionRepos: null,
uninstallAndDropDb: false,
maxStars: 5,
user: null
}
},
watch: {
searchTerm(term) {
this.filterExtensions(term, this.tab)
}
},
methods: {
handleTabChanged: function (tab) {
this.filterExtensions(this.searchTerm, tab)
},
filterExtensions: function (term, tab) {
// Filter the extensions list
function extensionNameContains(searchTerm) {
return function (extension) {
return (
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
extension.shortDescription
?.toLowerCase()
.includes(searchTerm.toLowerCase())
)
}
}
this.filteredExtensions = this.extensions
.filter(e => (tab === 'installed' ? e.isInstalled : true))
.filter(e =>
tab === 'installed'
? e.isActive
? true
: !!this.g.user.admin
: true
)
.filter(e => (tab === 'featured' ? e.isFeatured : true))
.filter(extensionNameContains(term))
this.tab = tab
},
installExtension: async function (release) {
const extension = this.selectedExtension
extension.inProgress = true
this.showUpgradeDialog = false
LNbits.api
.request(
'POST',
`/api/v1/extension?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey,
{
ext_id: extension.id,
archive: release.archive,
source_repo: release.source_repo
}
)
.then(response => {
extension.isAvailable = true
extension.isInstalled = true
extension.installedRelease = release
this.toggleExtension(extension)
extension.inProgress = false
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
uninstallExtension: async function () {
const extension = this.selectedExtension
this.showUpgradeDialog = false
this.showUninstallDialog = false
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.isAvailable = false
extension.isInstalled = false
extension.inProgress = false
extension.installedRelease = null
this.filteredExtensions = this.extensions.concat([])
this.handleTabChanged('installed')
this.tab = 'installed'
this.$q.notify({
type: 'positive',
message: 'Extension uninstalled!'
})
if (this.uninstallAndDropDb) {
this.showDropDb()
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
extension.inProgress = false
})
},
dropExtensionDb: async function () {
const extension = this.selectedExtension
this.showUpgradeDialog = false
this.showDropDbDialog = false
this.dropDbExtensionId = ''
extension.inProgress = true
LNbits.api
.request(
'DELETE',
`/api/v1/extension/${extension.id}/db?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
.then(response => {
extension.installedRelease = null
extension.inProgress = false
extension.hasDatabaseTables = false
this.$q.notify({
type: 'positive',
message: 'Extension DB deleted!'
})
})
.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
this.uninstallAndDropDb = false
},
showDropDb: function () {
this.showDropDbDialog = 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,
repo: release.repo
}
release.inProgress = false
release.error = null
release.loaded = false
release.isInstalled = this.isInstalledVersion(
this.selectedExtension,
release
)
if (release.isInstalled) {
repos[release.source_repo].isInstalled = true
}
repos[release.source_repo].releases.push(release)
return repos
}, {})
} catch (error) {
LNbits.utils.notifyApiError(error)
extension.inProgress = false
}
},
hasNewVersion: function (extension) {
if (extension.installedRelease && extension.latestRelease) {
return (
extension.installedRelease.version !==
extension.latestRelease.version
)
}
},
isInstalledVersion: function (extension, release) {
if (extension.installedRelease) {
return (
extension.installedRelease.source_repo === release.source_repo &&
extension.installedRelease.version === release.version
)
}
},
getReleaseIcon: function (release) {
if (!release.is_version_compatible) return 'block'
if (release.isInstalled) return 'download_done'
return 'download'
},
getReleaseIconColor: function (release) {
if (!release.is_version_compatible) return 'text-red'
if (release.isInstalled) return 'text-green'
return ''
},
getGitHubReleaseDetails: async function (release) {
if (!release.is_github_release || release.loaded) {
return
}
const [org, repo] = release.source_repo.split('/')
release.inProgress = true
try {
const {data} = await LNbits.api.request(
'GET',
`/api/v1/extension/release/${org}/${repo}/${release.version}?usr=${this.g.user.id}`,
this.g.user.wallets[0].adminkey
)
release.loaded = true
release.is_version_compatible = data.is_version_compatible
release.min_lnbits_version = data.min_lnbits_version
release.warning = data.warning
} catch (error) {
console.warn(error)
release.error = error
LNbits.utils.notifyApiError(error)
} finally {
release.inProgress = false
}
}
},
created: function () {
this.extensions = JSON.parse('{{extensions | tojson | safe}}').map(e => ({
...e,
inProgress: false
}))
this.filteredExtensions = this.extensions.concat([])
for (let i = 0; i < this.filteredExtensions.length; i++) {
if (this.filteredExtensions[i].isInstalled != false) {
this.handleTabChanged('installed')
this.tab = 'installed'
}
}
if (window.user) {
this.user = LNbits.map.user(window.user)
}
},
mixins: [windowMixin]
})
</script>
{% endblock %}
+46 -56
View File
@@ -3,6 +3,7 @@
{% 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">
{% if lnurl or LNBITS_NEW_ACCOUNTS_ALLOWED %}
<q-card>
<q-card-section>
{% if lnurl %}
@@ -12,55 +13,35 @@
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
>
Press to claim bitcoin
</q-btn>
{% else %}
v-text="$t('press_to_claim')"
></q-btn>
{% elif LNBITS_NEW_ACCOUNTS_ALLOWED %}
<q-form @submit="createWallet" class="q-gutter-md">
<q-input
filled
dense
v-model="walletName"
label="Name your {{SITE_TITLE}} wallet *"
:label='$t("name_your_wallet", { name: "{{ SITE_TITLE }} *" })'
></q-input>
<q-btn
unelevated
color="primary"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
>
:label="$t('add_wallet')"
></q-btn>
</q-form>
{% endif %}
</q-card-section>
</q-card>
{% 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>
Easy to set up and lightweight, LNbits can run on any
lightning-network funding source, currently supporting LND,
c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian
solution for others.
</p>
<p>
Each wallet has its own API keys and there is no limit to the number
of wallets you can make. Being able to partition funds makes LNbits
a useful tool for money management and as a development tool.
</p>
<p>
Extensions add extra functionality to LNbits so you can experiment
with a range of cutting-edge technologies on the lightning network.
We have made developing extensions as easy as possible, and as a
free and open-source project, we encourage people to develop and
submit their own.
</p>
<p v-text="$t('lnbits_description')"></p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
@@ -69,8 +50,8 @@
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
:label="$t('view_github')"
></q-btn>
<q-btn
outline
color="grey"
@@ -78,11 +59,13 @@
href="https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
:label="$t('donate')"
></q-btn>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION | safe}}</p>
<div v-else>
<div v-html="formatDescription"></div>
</div>
</q-card-section>
</q-card>
</div>
@@ -94,7 +77,7 @@
<q-btn
flat
color="secondary"
label="Runs on"
:label="$t('runs_on')"
class="full-width"
></q-btn>
<div class="row">
@@ -135,24 +118,6 @@
</div>
</div>
<div class="row">
<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>
<div class="col q-pl-md">
<a href="https://t.me/lntxbot">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/lntxbot.png' : '/static/images/lntxbotl.png'"
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<a href="https://github.com/rootzoll/raspiblitz">
@@ -163,6 +128,16 @@
</a>
</div>
<div class="col q-pl-md">
<a href="https://start9.com/">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/start9.png' : '/static/images/start9l.png'"
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<a href="https://getumbrel.com/">
<q-img
contain
@@ -170,9 +145,7 @@
></q-img>
</a>
</div>
</div>
<div class="row">
<div class="col">
<div class="col q-pl-md">
<a href="https://mynodebtc.com">
<q-img
contain
@@ -180,7 +153,24 @@
></q-img>
</a>
</div>
<div class="col q-pl-md">&nbsp;</div>
</div>
<div class="row">
<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>
<div class="col q-pl-md">
<a href="https://voltage.cloud">
<q-img
contain
:src="($q.dark.isActive) ? '/static/images/voltage.png' : '/static/images/voltagel.png'"
></q-img>
</a>
</div>
</div>
</div>
+259 -156
View File
@@ -37,8 +37,8 @@
>
<q-input
filled
label="{{LNBITS_DENOMINATION}} to credit"
hint="Press Enter to credit account"
:label='$t("credit_label", { denomination: "{{LNBITS_DENOMINATION}}"})'
:hint="$t('credit_hint')"
v-model="scope.value"
dense
autofocus
@@ -63,8 +63,8 @@
color="primary"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
>
:label="$t('paste_request')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -72,8 +72,8 @@
color="primary"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
>
:label="$t('create_invoice')"
></q-btn>
</div>
<div class="col">
<q-btn
@@ -81,8 +81,11 @@
color="secondary"
icon="photo_camera"
@click="showCamera"
>scan
<q-tooltip>Use camera to scan an invoice/QR</q-tooltip>
:label="$t('scan')"
>
<q-tooltip
>{% raw %}{{$t('camera_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
@@ -92,12 +95,18 @@
<q-card-section>
<div class="row items-center no-wrap q-mb-sm">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Transactions</h5>
<h5
class="text-subtitle1 q-my-none"
:v-text="$t('transactions')"
></h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV"
>Export to CSV</q-btn
>
<q-btn
flat
color="grey"
@click="exportCSV"
:label="$t('export_csv')"
></q-btn>
<!--<q-btn v-if="pendingPaymentsExist" dense flat round icon="update" color="grey" @click="checkPendingPayments">
<q-tooltip>Check pending</q-tooltip>
</q-btn>-->
@@ -109,30 +118,33 @@
color="grey"
@click="showChart"
>
<q-tooltip>Show chart</q-tooltip>
<q-tooltip
>{% raw %}{{$t('chart_tooltip')}}{% endraw %}</q-tooltip
>
</q-btn>
</div>
</div>
<q-input
v-if="payments.length > 10"
filled
dense
clearable
v-model="paymentsTable.filter"
debounce="300"
placeholder="Search by tag, memo, amount"
:placeholder="$t('search_by_tag_memo_amount')"
class="q-mb-md"
>
</q-input>
<q-table
dense
flat
:data="filteredPayments"
:data="payments"
:row-key="paymentTableRowKey"
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
no-data-label="No transactions made yet"
:no-data-label="$t('no_transactions')"
:filter="paymentsTable.filter"
:loading="paymentsTable.loading"
@request="fetchPayments"
>
{% raw %}
<template v-slot:header="props">
@@ -159,11 +171,11 @@
color="grey"
@click="props.expand = !props.expand"
>
<q-tooltip>Pending</q-tooltip>
<q-tooltip>{{$t('pending')}}</q-tooltip>
</q-icon>
</q-td>
<q-td
key="memo"
key="time"
:props="props"
style="white-space: normal; word-break: break-all"
>
@@ -180,27 +192,36 @@
</a>
</q-badge>
{{ props.row.memo }}
<br />
<i>
{{ props.row.dateFrom }}<q-tooltip
>{{ props.row.date }}</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
}}
</q-td>
<q-td auto-width key="sat" v-else :props="props">
{{ props.row.fsat }}
</q-td>
<q-td auto-width key="fee" :props="props">
{{ props.row.fee }}
<q-td auto-width key="amount" v-else :props="props">
{{ props.row.fsat }}<br />
<i v-if="props.row.extra.wallet_fiat_currency">
{{ props.row.extra.wallet_fiat_currency }} {{
props.row.extra.wallet_fiat_amount }}
<br />
</i>
<i v-if="props.row.extra.fiat_currency">
{{ props.row.extra.fiat_currency }} {{
props.row.extra.fiat_amount }}
</i>
</q-td>
</q-tr>
@@ -209,7 +230,7 @@
<div class="text-center q-mb-lg">
<div v-if="props.row.isIn && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Invoice waiting to be paid
<span v-text="$t('invoice_waiting')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -219,11 +240,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>
@@ -232,15 +251,15 @@
outline
color="grey"
@click="copyText(props.row.bolt11)"
>Copy invoice</q-btn
>
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
>Close</q-btn
>
:label="$t('close')"
></q-btn>
</div>
</div>
<div v-else-if="props.row.isPaid && props.row.isIn">
@@ -249,7 +268,7 @@
:name="'call_received'"
:color="'green'"
></q-icon>
Payment Received
<span v-text="$t('payment_received')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -260,14 +279,14 @@
:name="'call_made'"
:color="'pink'"
></q-icon>
Payment Sent
<span v-text="$t('payment_sent')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
</div>
<div v-else-if="props.row.isOut && props.row.pending">
<q-icon name="settings_ethernet" color="grey"></q-icon>
Outgoing payment pending
<span v-text="$t('outgoing_payment_pending')"></span>
<lnbits-payment-details
:payment="props.row"
></lnbits-payment-details>
@@ -304,26 +323,16 @@
<q-expansion-item
group="extras"
icon="crop_free"
label="Drain Funds"
:label="$t('drain_funds')"
>
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping
everything from this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="lightning:{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
<lnbits-qrcode
:value="lightning:{{wallet.lnurlwithdraw_full}}"
></lnbits-qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep
pulling the funds continuously from here after the first
withdraw.
</p>
<p v-text="$t('drain_funds_desc')"></p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -333,18 +342,13 @@
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
:label="$t('export_to_phone')"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access.
You can scan it from your phone to open your wallet from
there.
</p>
<p v-text="$t('export_to_phone_desc')"></p>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</q-card-section>
</q-card>
@@ -353,48 +357,76 @@
<q-expansion-item
group="extras"
icon="edit"
label="Rename wallet"
:label="$t('rename_wallet')"
>
<q-card>
<q-card-section>
<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"
@click="updateWalletName()"
>Update name</q-btn
>
:label="$t('update_name')"
@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="remove_circle"
label="Delete wallet"
icon="attach_money"
:label="$t('fiat_tracking')"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<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"
icon="remove_circle"
:label="$t('delete_wallet')"
>
<q-card>
<q-card-section>
<p v-text="$t('delete_wallet_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
@click="deleteWallet()"
:label="$t('delete_wallet')"
></q-btn>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -430,7 +462,7 @@
</div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
<q-dialog v-model="receive.show">
{% raw %}
<q-card
v-if="!receive.paymentReq"
@@ -445,7 +477,7 @@
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:label="$t('amount')+' ({{LNBITS_DENOMINATION}}) *'"
mask="#.##"
fill-mask="0"
reverse-fill-mask
@@ -459,7 +491,7 @@
dense
v-model="receive.unit"
type="text"
label="Unit"
:label="$t('unit')"
:options="receive.units"
></q-select>
<q-input
@@ -467,7 +499,7 @@
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:label="$t('amount') + ' (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
@@ -482,7 +514,7 @@
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
:label="$t('memo')"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
@@ -493,13 +525,17 @@
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
{{$t('withdraw_from')}} {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
<span v-else v-text="$t('create_invoice')"></span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
@@ -512,21 +548,26 @@
<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>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
<q-btn
outline
color="grey"
@click="copyText(receive.paymentReq)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</div>
</q-card>
{% endraw %}
@@ -545,24 +586,42 @@
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong v-text="$t('memo')">:</strong> {{
parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
unelevated
color="primary"
@click="payInvoice"
:label="$t('pay')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('not_enough_funds')"
unelevated
disabled
color="yellow"
text-color="black"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</div>
<div v-else-if="parse.lnurlauth">
@@ -584,9 +643,13 @@
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -657,9 +720,13 @@
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
:label="$t('cancel')"
v-close-popup
flat
color="grey"
class="q-ml-auto"
></q-btn>
</div>
</q-form>
{% endraw %}
@@ -675,7 +742,8 @@
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
:label="$t('paste_invoice_label')"
ref="textArea"
>
</q-input>
<div class="row q-mt-lg">
@@ -684,11 +752,26 @@
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
:label="$t('read')"
></q-btn>
<q-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
>{% raw %}{{$t('paste_from_clipboard')}}{% endraw
%}</q-tooltip
></q-icon
>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-form>
<div v-else>
@@ -700,8 +783,13 @@
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
<q-btn
:label="$t('cancel')"
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
>
</q-btn>
</div>
</div>
@@ -719,9 +807,13 @@
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
<q-btn
@click="closeCamera"
flat
color="grey"
class="q-ml-auto"
:label="$t('cancel')"
></q-btn>
</div>
</q-card>
</q-dialog>
@@ -729,57 +821,68 @@
<q-dialog v-model="paymentsChart.show">
<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-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Warning</h6>
<p>
Login functionality to be released in a future update, for now,
<strong
>make sure you bookmark this page for future access to your
wallet</strong
>!
</p>
<p>
This service is in BETA, and we hold no responsibility for people
losing access to funds. {% if service_fee > 0 %} To encourage you to
run your own LNbits installation, any balance on {% raw %}{{
disclaimerDialog.location.host }}{% endraw %} will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week. {% endif
%}
</p>
<p v-text="$t('disclaimer_dialog')"></p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn
>
:label="$t('copy_wallet_url')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
:label="$t('i_understand')"
></q-btn>
</div>
</q-card>
</q-dialog>
@@ -0,0 +1,312 @@
<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>
{% raw %}
<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">
{{props.row.name}}
</div>
<div class="text-caption" v-if="props.row.short_id">
{{ props.row.short_id }}
<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"
>
{{ 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>
{% endraw %}
</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">
{% raw %}
<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">
{{ props.row.alias }}
</div>
<div class="row items-center q-gutter-sm">
<q-badge
:style="`background-color: #${props.row.color}`"
class="text-bold"
>
#{{ props.row.color }}
</q-badge>
<div class="text-bold">
{{ 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>
{% endraw %}
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,70 @@
<q-tab-panel name="dashboard">
<q-card-section class="q-pa-none">
{% raw %}
<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="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>
{% endraw %}
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,316 @@
<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>
{% raw %}
<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">
{{ transactionDetailsDialog.data.payment_hash }}
<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">
{{ transactionDetailsDialog.data.preimage }}
<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>
{% endraw %}
</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"
>
{% raw %}
<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"
>
{{ 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>
{% endraw %}
</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"
>
{% raw %}
<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>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
</div>
</q-card-section>
</q-tab-panel>
+377
View File
@@ -0,0 +1,377 @@
{% 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="/core/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)
params.set('usr', this.g.user.id)
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="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="/core/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 %}
+98 -19
View File
@@ -1,21 +1,54 @@
import os
import time
from http import HTTPStatus
from shutil import make_archive
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.decorators import check_admin, check_super_user
from lnbits.server import server_restart
from lnbits.settings import AdminSettings, EditableSettings
from lnbits.settings import AdminSettings, UpdateSettings, settings
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/settings/", response_model=Optional[AdminSettings])
async def api_get_settings(
user: User = Depends(check_admin),
) -> Optional[AdminSettings]:
@@ -23,18 +56,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)],
@@ -44,7 +79,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)],
@@ -54,21 +89,65 @@ 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"}
@admin_router.get(
"/admin/api/v1/backup/",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_super_user)],
response_class=FileResponse,
)
async def api_download_backup() -> FileResponse:
last_filename = "lnbits-backup"
filename = f"lnbits-backup-{int(time.time())}.zip"
db_url = settings.lnbits_database_url
pg_backup_filename = f"{settings.lnbits_data_folder}/lnbits-database.dmp"
is_pg = db_url and db_url.startswith("postgres://")
if is_pg:
p = urlparse(db_url)
command = (
f"pg_dump --host={p.hostname} "
f"--dbname={p.path.replace('/', '')} "
f"--username={p.username} "
"--no-password "
"--format=c "
f"--file={pg_backup_filename}"
)
proc = Popen(
command, shell=True, env={**os.environ, "PGPASSWORD": p.password or ""}
)
proc.wait()
make_archive(last_filename, "zip", settings.lnbits_data_folder)
# cleanup pg_dump file
if is_pg:
proc = Popen(f"rm {pg_backup_filename}", shell=True)
proc.wait()
return FileResponse(
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
)
+472 -212
View File
@@ -1,61 +1,102 @@
import asyncio
import base64
import hashlib
import json
import time
import uuid
from http import HTTPStatus
from io import BytesIO
from typing import Dict, Optional, Tuple, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, unquote, urlencode, urlparse, urlunparse
import async_timeout
import httpx
import pyqrcode
from fastapi import (
APIRouter,
Body,
Depends,
Header,
Query,
Request,
Response,
WebSocket,
WebSocketDisconnect,
)
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
from loguru import logger
from pydantic import BaseModel
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
from starlette.responses import RedirectResponse, StreamingResponse
from lnbits import bolt11, lnurl
from lnbits.core.models import Payment, Wallet
from lnbits.core.db import core_app_extra, db
from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import (
ConversionData,
CreateInvoice,
CreateLnurl,
CreateLnurlAuth,
CreateWallet,
CreateWebPushSubscription,
DecodePayment,
Payment,
PaymentFilters,
PaymentHistoryPoint,
Query,
User,
Wallet,
WalletType,
WebPushSubscription,
)
from lnbits.db import Filters, Page
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
get_key_type,
parse_filters,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for
from lnbits.settings import get_wallet_class, settings
from lnbits.extension_manager import (
CreateExtension,
Extension,
ExtensionRelease,
InstallableExtension,
fetch_github_release_config,
get_valid_extensions,
)
from lnbits.helpers import generate_filter_params_openapi, url_for
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from .. import core_app, db
from ..crud import (
DateTrunc,
add_installed_extension,
create_account,
create_tinyurl,
create_wallet,
create_webpush_subscription,
delete_dbversion,
delete_installed_extension,
delete_tinyurl,
delete_wallet,
delete_webpush_subscription,
drop_extension_db,
get_dbversions,
get_payments,
get_payments_history,
get_payments_paginated,
get_standalone_payment,
get_tinyurl,
get_tinyurl_by_url,
get_total_balance,
get_wallet_for_key,
get_webpush_subscription,
save_balance_check,
update_pending_payments,
update_wallet,
)
from ..services import (
@@ -70,15 +111,17 @@ from ..services import (
)
from ..tasks import api_invoice_listeners
api_router = APIRouter()
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
@api_router.get("/api/v1/health", status_code=HTTPStatus.OK)
async def health():
return
@core_app.get("/api/v1/wallet")
@api_router.get("/api/v1/wallet")
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet_type == 0:
if wallet.wallet_type == WalletType.admin:
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
@@ -88,8 +131,8 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
@core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(
@api_router.put("/api/v1/wallet/{new_name}")
async def api_update_wallet_name(
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
await update_wallet(wallet.wallet.id, new_name)
@@ -100,89 +143,147 @@ async def api_update_wallet(
}
@core_app.get("/api/v1/payments")
async def api_payments(
limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
@api_router.patch("/api/v1/wallet", response_model=Wallet)
async def api_update_wallet(
name: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
pendingPayments = await get_payments(
return await update_wallet(wallet.wallet.id, name, currency)
@api_router.delete("/api/v1/wallet")
async def api_delete_wallet(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> None:
await delete_wallet(
user_id=wallet.wallet.user,
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
limit=limit,
offset=offset,
)
for payment in pendingPayments:
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
@api_router.post("/api/v1/wallet", response_model=Wallet)
async def api_create_wallet(
data: CreateWallet,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> Wallet:
return await create_wallet(user_id=wallet.wallet.user, wallet_name=data.name)
@api_router.post("/api/v1/account", response_model=Wallet)
async def api_create_account(data: CreateWallet) -> Wallet:
if not settings.new_accounts_allowed:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Account creation is disabled.",
)
account = await create_account()
return await create_wallet(user_id=account.id, wallet_name=data.name)
@api_router.get(
"/api/v1/payments",
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
filters=filters,
)
class CreateInvoiceData(BaseModel):
out: Optional[bool] = True
amount: float = Query(None, ge=0)
memo: Optional[str] = None
unit: Optional[str] = "sat"
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
internal: Optional[bool] = False
bolt11: Optional[str] = None
@api_router.get(
"/api/v1/payments/history",
name="Get payments history",
response_model=List[PaymentHistoryPoint],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_history(
wallet: WalletTypeInfo = Depends(get_key_type),
group: DateTrunc = Query("day"),
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
return await get_payments_history(wallet.wallet.id, group, filters)
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
@api_router.get(
"/api/v1/payments/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_paginated(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
await update_pending_payments(wallet.wallet.id)
page = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
return page
async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
try:
description_hash = (
bytes.fromhex(data.description_hash) if data.description_hash else b""
)
unhashed_description = (
bytes.fromhex(data.unhashed_description)
if data.unhashed_description
else b""
)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
)
if data.description_hash:
try:
description_hash = bytes.fromhex(data.description_hash)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
)
if data.unhashed_description:
try:
unhashed_description = bytes.fromhex(data.unhashed_description)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
)
# do not save memo if description_hash or unhashed_description is set
memo = ""
else:
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.unit == "sat":
amount = int(data.amount)
else:
assert data.unit is not None, "unit not set"
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
async with db.connect() as conn:
try:
payment_hash, payment_request = await create_invoice(
wallet_id=wallet.id,
amount=amount,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
conn=conn,
)
# NOTE: we get the checking_id with a seperate query because create_invoice
# does not return it and it would be a big hustle to change its return type
# (used across extensions)
payment_db = await get_standalone_payment(payment_hash, conn=conn)
assert payment_db is not None, "payment not found"
checking_id = payment_db.checking_id
except InvoiceFailure as e:
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
@@ -225,7 +326,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"checking_id": checking_id,
"lnurl_response": lnurl_response,
}
@@ -249,15 +350,24 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
}
@core_app.post(
@api_router.post(
"/api/v1/payments",
summary="Create or pay an invoice",
description="""
This endpoint can be used both to generate and pay a BOLT11 invoice.
To generate a new invoice for receiving funds into the authorized account,
specify at least the first four fields in the POST body: `out: false`,
`amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
already in the authorized account, specify `out: true` and use the `bolt11`
field to supply the BOLT11 invoice to be paid.
""",
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoiceData = Body(...),
invoiceData: CreateInvoice = Body(...),
):
if invoiceData.out is True and wallet.wallet_type == 0:
if invoiceData.out is True and wallet.wallet_type == WalletType.admin:
if not invoiceData.bolt11:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -276,17 +386,9 @@ async def api_payments_create(
)
class CreateLNURLData(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
@core_app.post("/api/v1/payments/lnurl")
@api_router.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
):
domain = urlparse(data.callback).netloc
@@ -322,13 +424,12 @@ async def api_payments_pay_lnurl(
if invoice.amount_msat != data.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
)
if invoice.description_hash != data.description_hash:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
detail=(
(
f"{domain} returned an invalid invoice. Expected"
f" {data.amount} msat, got {invoice.amount_msat}."
),
),
)
extra = {}
@@ -366,39 +467,24 @@ async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
logger.debug(f"adding sse listener for wallet: {uid}")
api_invoice_listeners[uid] = payment_queue
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None:
while True:
try:
async with async_timeout.timeout(1):
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment receieved", payment)
await send_queue.put(("payment-received", payment))
except asyncio.TimeoutError:
pass
task = asyncio.create_task(payment_received())
try:
while True:
if await request.is_disconnected():
await request.close()
break
typ, data = await send_queue.get()
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
yield dict(data=jdata, event=typ)
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment received", payment)
yield dict(data=payment.json(), event="payment-received")
except asyncio.CancelledError:
logger.debug(f"removing listener for wallet {uid}")
except Exception as exc:
logger.error(f"Error in sse: {exc}")
finally:
api_invoice_listeners.pop(uid)
task.cancel()
return
@core_app.get("/api/v1/payments/sse")
@api_router.get("/api/v1/payments/sse")
async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
@@ -409,14 +495,16 @@ async def api_payments_sse(
)
@core_app.get("/api/v1/payments/{payment_hash}")
# TODO: refactor this route into a public and admin one
@api_router.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
wallet = await get_wallet_for_key(X_Api_Key) if isinstance(X_Api_Key, str) else None
wallet = wallet if wallet and not wallet.deleted else None
# we have to specify the wallet id here, because postgres and sqlite return
# internal payments in different order and get_standalone_payment otherwise
# just fetches the first one, causing unpredictable results
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
@@ -453,12 +541,12 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
return {"paid": not payment.pending, "preimage": payment.preimage}
@core_app.get("/api/v1/lnurlscan/{code}")
@api_router.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try:
url = lnurl.decode(code)
domain = urlparse(url).netloc
except:
except Exception:
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
@@ -483,10 +571,12 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
assert lnurlauth_key.verifying_key
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(follow_redirects=True) as client:
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
@@ -547,10 +637,10 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
for [k, v] in metadata:
if k == "text/plain":
params.update(description=v)
if k == "image/jpeg;base64" or k == "image/png;base64":
data_uri = "data:" + k + "," + v
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = f"data:{k},{v}"
params.update(image=data_uri)
if k == "text/email" or k == "text/identifier":
if k in ("text/email", "text/identifier"):
params.update(targetUser=v)
params.update(commentAllowed=data.get("commentAllowed", 0))
@@ -566,45 +656,28 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
return params
class DecodePayment(BaseModel):
data: str
@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
async def api_payments_decode(data: DecodePayment, response: Response):
@api_router.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
async def api_payments_decode(data: DecodePayment) -> JSONResponse:
payment_str = data.data
try:
if payment_str[:5] == "LNURL":
url = lnurl.decode(payment_str)
return {"domain": url}
return JSONResponse({"domain": url})
else:
invoice = bolt11.decode(payment_str)
return {
"payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat,
"description": invoice.description,
"description_hash": invoice.description_hash,
"payee": invoice.payee,
"date": invoice.date,
"expiry": invoice.expiry,
"secret": invoice.secret,
"route_hints": invoice.route_hints,
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
}
except:
response.status_code = HTTPStatus.BAD_REQUEST
return {"message": "Failed to decode"}
return JSONResponse(invoice.data)
except Exception as exc:
return JSONResponse(
{"message": f"Failed to decode: {str(exc)}"},
status_code=HTTPStatus.BAD_REQUEST,
)
class Callback(BaseModel):
callback: str = Query(...)
@core_app.post("/api/v1/lnurlauth")
@api_router.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(callback.callback, wallet=wallet)
err = await perform_lnurlauth(data.callback, wallet=wallet)
if err:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
@@ -612,18 +685,18 @@ async def api_perform_lnurlauth(
return ""
@core_app.get("/api/v1/currencies")
@api_router.get("/api/v1/currencies")
async def api_list_currencies_available():
if len(settings.lnbits_allowed_currencies) > 0:
return [
item
for item in currencies.keys()
if item.upper() in settings.lnbits_allowed_currencies
]
return list(currencies.keys())
class ConversionData(BaseModel):
from_: str = Field("sat", alias="from")
amount: float
to: str = Query("usd")
@core_app.post("/api/v1/conversion")
@api_router.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData):
output = {}
if data.from_ == "sat":
@@ -641,8 +714,8 @@ async def api_fiat_as_sats(data: ConversionData):
return output
@core_app.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(request: Request, data):
@api_router.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(data):
qr = pyqrcode.create(data)
stream = BytesIO()
qr.svg(stream, scale=3)
@@ -662,60 +735,205 @@ async def img(request: Request, data):
)
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
async def api_auditor():
WALLET = get_wallet_class()
total_balance = await get_total_balance()
error_message, node_balance = await WALLET.status()
if not error_message:
delta = node_balance - total_balance
else:
node_balance, delta = None, None
return {
"node_balance_msats": int(node_balance),
"lnbits_balance_msats": int(total_balance),
"delta_msats": int(delta),
"timestamp": int(time.time()),
}
##################UNIVERSAL WEBSOCKET MANAGER########################
@core_app.websocket("/api/v1/ws/{item_id}")
@api_router.websocket("/api/v1/ws/{item_id}")
async def websocket_connect(websocket: WebSocket, item_id: str):
await websocketManager.connect(websocket)
await websocketManager.connect(websocket, item_id)
try:
while True:
data = await websocket.receive_text()
await websocket.receive_text()
except WebSocketDisconnect:
websocketManager.disconnect(websocket)
@core_app.post("/api/v1/ws/{item_id}")
@api_router.post("/api/v1/ws/{item_id}")
async def websocket_update_post(item_id: str, data: str):
try:
await websocketUpdater(item_id, data)
return {"sent": True, "data": data}
except:
except Exception:
return {"sent": False, "data": data}
@core_app.get("/api/v1/ws/{item_id}/{data}")
@api_router.get("/api/v1/ws/{item_id}/{data}")
async def websocket_update_get(item_id: str, data: str):
try:
await websocketUpdater(item_id, data)
return {"sent": True, "data": data}
except:
except Exception:
return {"sent": False, "data": data}
############################TINYURL##################################
@api_router.post("/api/v1/extension")
async def api_install_extension(
data: CreateExtension, user: User = Depends(check_admin)
):
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive
)
if not release:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
if not release.is_version_compatible:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
)
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
ext_info.download_archive()
try:
ext_info.extract_archive()
extension = Extension.from_installable_ext(ext_info)
db_version = (await get_dbversions()).get(data.ext_id, 0)
await migrate_extension_database(extension, db_version)
await add_installed_extension(ext_info)
# call stop while the old routes are still active
await stop_extension_background_work(data.ext_id, user.id)
if data.ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [data.ext_id]
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
if extension.upgrade_hash:
ext_info.nofiy_upgrade()
return extension
except Exception as ex:
logger.warning(ex)
ext_info.clean_extension_files()
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=(
f"Failed to install extension {ext_info.id} "
f"({ext_info.installed_version})."
),
)
@core_app.post("/api/v1/tinyurl")
@api_router.delete("/api/v1/extension/{ext_id}")
async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)):
installable_extensions = await InstallableExtension.get_installable_extensions()
extensions = [e for e in installable_extensions if e.id == ext_id]
if len(extensions) == 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
# check that other extensions do not depend on this one
for valid_ext_id in list(map(lambda e: e.code, get_valid_extensions())):
installed_ext = next(
(ext for ext in installable_extensions if ext.id == valid_ext_id), None
)
if installed_ext and ext_id in installed_ext.dependencies:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"Cannot uninstall. Extension '{installed_ext.name}' "
"depends on this one."
),
)
try:
# call stop while the old routes are still active
await stop_extension_background_work(ext_id, user.id)
if ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [ext_id]
for ext_info in extensions:
ext_info.clean_extension_files()
await delete_installed_extension(ext_id=ext_info.id)
logger.success(f"Extension '{ext_id}' uninstalled.")
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
@api_router.get(
"/api/v1/extension/{ext_id}/releases", dependencies=[Depends(check_admin)]
)
async def get_extension_releases(ext_id: str):
try:
extension_releases: List[
ExtensionRelease
] = await InstallableExtension.get_extension_releases(ext_id)
return extension_releases
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
@api_router.get(
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
dependencies=[Depends(check_admin)],
)
async def get_extension_release(org: str, repo: str, tag_name: str):
try:
config = await fetch_github_release_config(org, repo, tag_name)
if not config:
return {}
return {
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
}
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
@api_router.delete(
"/api/v1/extension/{ext_id}/db",
dependencies=[Depends(check_admin)],
)
async def delete_extension_db(ext_id: str):
try:
db_version = (await get_dbversions()).get(ext_id, None)
if not db_version:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
await drop_extension_db(ext_id=ext_id)
await delete_dbversion(ext_id=ext_id)
logger.success(f"Database removed for extension '{ext_id}'")
except HTTPException as ex:
logger.error(ex)
raise ex
except Exception as ex:
logger.error(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'",
)
@api_router.post(
"/api/v1/tinyurl",
name="Tinyurl",
description="creates a tinyurl",
)
async def api_create_tinyurl(
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
):
@@ -726,13 +944,17 @@ async def api_create_tinyurl(
if tinyurl.wallet == wallet.wallet.inkey:
return tinyurl
return await create_tinyurl(url, endless, wallet.wallet.inkey)
except:
except Exception:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
)
@core_app.get("/api/v1/tinyurl/{tinyurl_id}")
@api_router.get(
"/api/v1/tinyurl/{tinyurl_id}",
name="Tinyurl",
description="get a tinyurl by id",
)
async def api_get_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
@@ -744,13 +966,17 @@ async def api_get_tinyurl(
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
except Exception:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
)
@core_app.delete("/api/v1/tinyurl/{tinyurl_id}")
@api_router.delete(
"/api/v1/tinyurl/{tinyurl_id}",
name="Tinyurl",
description="delete a tinyurl by id",
)
async def api_delete_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
@@ -763,22 +989,56 @@ async def api_delete_tinyurl(
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
except Exception:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
)
@core_app.get("/t/{tinyurl_id}")
@api_router.get(
"/t/{tinyurl_id}",
name="Tinyurl",
description="redirects a tinyurl by id",
)
async def api_tinyurl(tinyurl_id: str):
try:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
response = RedirectResponse(url=tinyurl.url)
return response
else:
return
except:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
response = RedirectResponse(url=tinyurl.url)
return response
else:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
)
@api_router.post("/api/v1/webpush", status_code=HTTPStatus.CREATED)
async def api_create_webpush_subscription(
request: Request,
data: CreateWebPushSubscription,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> WebPushSubscription:
subscription = json.loads(data.subscription)
endpoint = subscription["endpoint"]
host = urlparse(str(request.url)).netloc
subscription = await get_webpush_subscription(endpoint, wallet.wallet.user)
if subscription:
return subscription
else:
return await create_webpush_subscription(
endpoint,
wallet.wallet.user,
data.subscription,
host,
)
@api_router.delete("/api/v1/webpush", status_code=HTTPStatus.OK)
async def api_delete_webpush_subscription(
request: Request,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
endpoint = unquote(
base64.b64decode(str(request.query_params.get("endpoint"))).decode("utf-8")
)
await delete_webpush_subscription(endpoint, wallet.wallet.user)
+397 -269
View File
@@ -1,49 +1,57 @@
import asyncio
from http import HTTPStatus
from typing import Optional
from typing import List, Optional
from urllib.parse import urlparse
from fastapi import 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 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 ...helpers import get_valid_extensions
from ...extension_manager import InstallableExtension, get_valid_extensions
from ...utils.exchange_rates import currencies
from ..crud import (
create_account,
create_wallet,
delete_wallet,
get_balance_check,
get_dbversions,
get_inactive_extensions,
get_installed_extensions,
get_user,
save_balance_notify,
update_installed_extension_state,
update_user_extension,
)
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")
@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("/robots.txt", response_class=HTMLResponse)
async def robots():
data = """
User-agent: *
@@ -52,18 +60,387 @@ async def robots():
return HTMLResponse(content=data, media_type="text/plain")
@core_html_routes.get(
"/extensions", name="core.extensions", response_class=HTMLResponse
@generic_router.get(
"/extensions", name="install.extensions", response_class=HTMLResponse
)
async def extensions(
async def extensions_install(
request: Request,
user: User = Depends(check_user_exists),
activate: str = Query(None),
deactivate: str = Query(None),
enable: str = Query(None),
disable: str = Query(None),
):
extension_to_enable = enable
extension_to_disable = disable
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
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 += [
e for e in installed_exts if e.id not in installed_exts_ids
]
for e in installable_exts:
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext:
e.installed_release = installed_ext.installed_release
# use the installed extension values
e.name = installed_ext.name
e.short_description = installed_ext.short_description
e.icon = installed_ext.icon
except Exception as ex:
logger.warning(ex)
installable_exts = []
try:
ext_id = activate or deactivate
if ext_id and user.admin:
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [deactivate]
elif activate:
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()))
inactive_extensions = await get_inactive_extensions()
db_version = await get_dbversions()
extensions = list(
map(
lambda ext: {
"id": ext.id,
"name": ext.name,
"icon": ext.icon,
"shortDescription": ext.short_description,
"stars": ext.stars,
"isFeatured": ext.featured,
"dependencies": ext.dependencies,
"isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version,
"isAvailable": ext.id in all_extensions,
"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
),
},
installable_exts,
)
)
return template_renderer().TemplateResponse(
"core/extensions.html",
{
"request": request,
"user": user.dict(),
"extensions": extensions,
},
)
except Exception as e:
logger.warning(e)
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
@generic_router.get(
"/wallet",
response_class=HTMLResponse,
description="show wallet page",
)
async def wallet(
request: Request,
usr: UUID4 = Query(...),
wal: Optional[UUID4] = Query(None),
):
user_id = usr.hex
user = await get_user(user_id)
if not user:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User does not exist."}
)
if not wal:
if len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id)
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={wallet.id}")
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={user.wallets[0].id}")
else:
wallet_id = wal.hex
userwallet = user.get_wallet(wallet_id)
if not userwallet or userwallet.deleted:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
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
logger.debug(f"Access user {user.id} wallet {userwallet.name}")
return template_renderer().TemplateResponse(
"core/wallet.html",
{
"request": request,
"user": user.dict(),
"wallet": userwallet.dict(),
"service_fee": settings.lnbits_service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
)
@generic_router.get("/withdraw", response_class=JSONResponse)
async def lnurl_full_withdraw(request: Request):
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."}
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."}
return {
"tag": "withdrawRequest",
"callback": url_for("/withdraw/cb", external=True, usr=user.id, wal=wallet.id),
"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]}"
),
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
}
@generic_router.get("/withdraw/cb", response_class=JSONResponse)
async def lnurl_full_withdraw_callback(request: Request):
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."}
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 Exception:
pass
asyncio.create_task(pay())
balance_notify = request.query_params.get("balanceNotify")
if balance_notify:
await save_balance_notify(wallet.id, balance_notify)
return {"status": "OK"}
@generic_router.get("/withdraw/notify/{service}")
async def lnurl_balance_notify(request: Request, service: str):
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)
@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)
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,
lightning_param,
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service
)
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@generic_router.get("/service-worker.js", response_class=FileResponse)
async def service_worker():
return FileResponse("lnbits/core/static/js/service-worker.js")
@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)
return {
"short_name": settings.lnbits_site_title,
"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@main/docs/logos/lnbits.png"
),
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
"scope": "/",
"theme_color": "#1F2234",
"shortcuts": [
{
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": f"/wallet?usr={usr}&wal={wallet.id}",
}
for wallet in user.wallets
],
"url_handlers": [{"origin": f"https://{host}"}],
}
@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)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"admin/index.html",
{
"request": request,
"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(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
@@ -78,261 +455,12 @@ async def extensions(
)
if extension_to_enable:
logger.info(f"Enabling extension: {extension_to_enable} for user {user.id}")
logger.info(f"Enabling extension: {extension_to_enable} for user {user_id}")
await update_user_extension(
user_id=user.id, extension=extension_to_enable, active=True
user_id=user_id, extension=extension_to_enable, active=True
)
elif extension_to_disable:
logger.info(f"Disabling extension: {extension_to_disable} for user {user.id}")
logger.info(f"Disabling extension: {extension_to_disable} for user {user_id}")
await update_user_extension(
user_id=user.id, extension=extension_to_disable, active=False
user_id=user_id, extension=extension_to_disable, active=False
)
# Update user as his extensions have been updated
if extension_to_enable or extension_to_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(
"/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>
""",
)
async def wallet(
request: Request = Query(None),
nme: Optional[str] = Query(None),
usr: Optional[UUID4] = Query(None),
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
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
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:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
return template_renderer().TemplateResponse(
"core/wallet.html",
{
"request": request,
"user": user.dict(), # type: ignore
"wallet": userwallet.dict(),
"service_fee": settings.lnbits_service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest", # type: ignore
},
)
@core_html_routes.get("/withdraw", response_class=JSONResponse)
async def lnurl_full_withdraw(request: Request):
user = await get_user(request.query_params.get("usr"))
if not user:
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.query_params.get("wal"))
if not wallet:
return {"status": "ERROR", "reason": "Wallet does not exist."}
return {
"tag": "withdrawRequest",
"callback": url_for("/withdraw/cb", external=True, usr=user.id, wal=wallet.id),
"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]}",
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
}
@core_html_routes.get("/withdraw/cb", response_class=JSONResponse)
async def lnurl_full_withdraw_callback(request: Request):
user = await get_user(request.query_params.get("usr"))
if not user:
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.query_params.get("wal"))
if not wallet:
return {"status": "ERROR", "reason": "Wallet does not exist."}
pr = request.query_params.get("pr")
async def pay():
try:
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
except:
pass
asyncio.create_task(pay())
balance_notify = request.query_params.get("balanceNotify")
if balance_notify:
await save_balance_notify(wallet.id, balance_notify)
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}")
async def lnurl_balance_notify(request: Request, service: str):
bc = await get_balance_check(request.query_params.get("wal"), service)
if bc:
await redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.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
asyncio.create_task(
redeem_lnurl_withdraw(
wallet.id,
request.query_params.get("lightning"),
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service
)
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}", # type: ignore
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")
@core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str):
user = await get_user(usr)
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return {
"short_name": settings.lnbits_site_title,
"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",
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
"scope": "/",
"theme_color": "#1F2234",
"shortcuts": [
{
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
}
for wallet in user.wallets
],
}
@core_html_routes.get("/admin", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_admin)):
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"admin/index.html",
{
"request": request,
"user": user.dict(),
"settings": settings.dict(),
"balance": balance,
},
)
+188
View File
@@ -0,0 +1,188 @@
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()
async with httpx.AsyncClient() 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")
+9 -21
View File
@@ -1,28 +1,18 @@
import asyncio
import datetime
from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import HTTPException
from fastapi import APIRouter, HTTPException
from loguru import logger
from starlette.requests import Request
from lnbits import bolt11
from .. import core_app
from ..crud import get_standalone_payment
from ..tasks import api_invoice_listeners
@core_app.get("/.well-known/lnurlp/{username}")
async def lnaddress(username: str, request: Request):
from lnbits.extensions.lnaddress.lnurl import lnurl_response # type: ignore
domain = urlparse(str(request.url)).netloc
return await lnurl_response(username, domain, request)
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)
@@ -35,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."
)
@@ -50,19 +39,18 @@ async def api_public_payment_longpolling(payment_hash):
response = None
async def payment_info_receiver(cancel_scope):
async for payment in payment_queue.get():
async def payment_info_receiver():
for payment in await payment_queue.get():
if payment.payment_hash == payment_hash:
nonlocal response
response = {"status": "paid"}
cancel_scope.cancel()
async def timeouter(cancel_scope):
await asyncio.sleep(45)
cancel_scope.cancel()
asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter())
cancel_scope = asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter(cancel_scope))
if response:
return response
+326 -64
View File
@@ -1,12 +1,17 @@
from __future__ import annotations
import asyncio
import datetime
import os
import re
import time
from contextlib import asynccontextmanager
from typing import Optional
from enum import Enum
from sqlite3 import Row
from typing import Any, Generic, List, Literal, Optional, Type, TypeVar
from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY
@@ -17,6 +22,48 @@ POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"
if settings.lnbits_database_url:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
DB_TYPE = COCKROACH
else:
DB_TYPE = POSTGRES
from psycopg2.extensions import DECIMAL, new_type, register_type
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
register_type(
new_type(
DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
register_type(new_type((1184, 1114), "TIMESTAMP2INT", _parse_timestamp))
else:
if not os.path.isdir(settings.lnbits_data_folder):
os.mkdir(settings.lnbits_data_folder)
logger.info(f"Created {settings.lnbits_data_folder}")
DB_TYPE = SQLITE
def compat_timestamp_placeholder():
if DB_TYPE == POSTGRES:
return "to_timestamp(?)"
elif DB_TYPE == COCKROACH:
return "cast(? AS timestamp)"
else:
return "?"
class Compat:
type: Optional[str] = "<inherited>"
@@ -66,6 +113,10 @@ class Compat:
return "BIGINT"
return "INT"
@property
def timestamp_placeholder(self) -> str:
return compat_timestamp_placeholder()
class Connection(Compat):
def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
@@ -85,17 +136,21 @@ class Connection(Compat):
# strip html
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
def cleanhtml(raw_html):
if isinstance(raw_html, str):
cleantext = re.sub(CLEANR, "", raw_html)
return cleantext
else:
return raw_html
# tuple to list and back to tuple
value_list = [values] if isinstance(values, str) else list(values)
values = tuple([cleanhtml(l) for l in value_list])
return values
raw_values = [values] if isinstance(values, str) else list(values)
values = []
for raw_value in raw_values:
if isinstance(raw_value, str):
values.append(re.sub(CLEANR, "", raw_value))
elif isinstance(raw_value, datetime.datetime):
ts = raw_value.timestamp()
if self.type == SQLITE:
values.append(int(ts))
else:
values.append(ts)
else:
values.append(raw_value)
return tuple(values)
async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute(
@@ -111,6 +166,51 @@ class Connection(Compat):
await result.close()
return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
if not filters:
filters = Filters()
clause = filters.where(where)
parsed_values = filters.values(values)
rows = await self.fetchall(
f"""
{query}
{clause}
{filters.order_by()}
{filters.pagination()}
""",
parsed_values,
)
if rows:
# no need for extra query if no pagination is specified
if filters.offset or filters.limit:
count = await self.fetchone(
f"""
SELECT COUNT(*) FROM (
{query}
{clause}
) as count
""",
parsed_values,
)
count = int(count[0])
else:
count = len(rows)
else:
count = 0
return Page(
data=[model.from_row(row) for row in rows] if model else rows,
total=count,
)
async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute(
self.rewrite_query(query), self.rewrite_values(values)
@@ -120,61 +220,17 @@ class Connection(Compat):
class Database(Compat):
def __init__(self, db_name: str):
self.name = db_name
self.schema = self.name
self.type = DB_TYPE
if settings.lnbits_database_url:
if DB_TYPE == SQLITE:
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
else:
database_uri = settings.lnbits_database_url
if database_uri.startswith("cockroachdb://"):
self.type = COCKROACH
else:
self.type = POSTGRES
import psycopg2
def _parse_timestamp(value, _):
if value is None:
return None
f = "%Y-%m-%d %H:%M:%S.%f"
if not "." in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
psycopg2.extensions.DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1184, 1114), "TIMESTAMP2INT", _parse_timestamp
)
)
else:
if os.path.isdir(settings.lnbits_data_folder):
self.path = os.path.join(
settings.lnbits_data_folder, f"{self.name}.sqlite3"
)
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {settings.lnbits_data_folder} was not created"
f" - please 'mkdir {settings.lnbits_data_folder}' and try again"
)
logger.trace(f"database {self.type} added for {self.name}")
self.schema = self.name
if self.name.startswith("ext_"):
self.schema = self.name[4:]
else:
@@ -183,11 +239,13 @@ class Database(Compat):
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager
async def connect(self):
await self.lock.acquire()
try:
async with self.engine.connect() as conn:
async with self.engine.connect() as conn: # type: ignore
async with conn.begin() as txn:
wconn = Connection(conn, txn, self.type, self.name, self.schema)
@@ -217,6 +275,17 @@ class Database(Compat):
await result.close()
return row
async def fetch_page(
self,
query: str,
where: Optional[List[str]] = None,
values: Optional[List[str]] = None,
filters: Optional[Filters] = None,
model: Optional[Type[TRowModel]] = None,
) -> Page[TRowModel]:
async with self.connect() as conn:
return await conn.fetch_page(query, where, values, filters, model)
async def execute(self, query: str, values: tuple = ()):
async with self.connect() as conn:
return await conn.execute(query, values)
@@ -224,3 +293,196 @@ class Database(Compat):
@asynccontextmanager
async def reuse_conn(self, conn: Connection):
yield conn
@classmethod
async def clean_ext_db_files(cls, ext_id: str) -> bool:
"""
If the extension DB is stored directly on the filesystem (like SQLite) then
delete the files and return True. Otherwise do nothing and return False.
"""
if DB_TYPE == SQLITE:
db_file = os.path.join(settings.lnbits_data_folder, f"ext_{ext_id}.sqlite3")
if os.path.isfile(db_file):
os.remove(db_file)
return True
return False
class Operator(Enum):
GT = "gt"
LT = "lt"
EQ = "eq"
NE = "ne"
GE = "ge"
LE = "le"
INCLUDE = "in"
EXCLUDE = "ex"
@property
def as_sql(self):
if self == Operator.EQ:
return "="
elif self == Operator.NE:
return "!="
elif self == Operator.INCLUDE:
return "IN"
elif self == Operator.EXCLUDE:
return "NOT IN"
elif self == Operator.GT:
return ">"
elif self == Operator.LT:
return "<"
elif self == Operator.GE:
return ">="
elif self == Operator.LE:
return "<="
else:
raise ValueError("Unknown SQL Operator")
class FromRowModel(BaseModel):
@classmethod
def from_row(cls, row: Row):
return cls(**dict(row))
class FilterModel(BaseModel):
__search_fields__: List[str] = []
__sort_fields__: Optional[List[str]] = None
T = TypeVar("T")
TModel = TypeVar("TModel", bound=BaseModel)
TRowModel = TypeVar("TRowModel", bound=FromRowModel)
TFilterModel = TypeVar("TFilterModel", bound=FilterModel)
class Page(BaseModel, Generic[T]):
data: list[T]
total: int
class Filter(BaseModel, Generic[TFilterModel]):
field: str
op: Operator = Operator.EQ
values: list[Any]
model: Optional[Type[TFilterModel]]
@classmethod
def parse_query(cls, key: str, raw_values: list[Any], model: Type[TFilterModel]):
# Key format:
# key[operator]
# e.g. name[eq]
if key.endswith("]"):
split = key[:-1].split("[")
if len(split) != 2:
raise ValueError("Invalid key")
field = split[0]
op = Operator(split[1])
else:
field = key
op = Operator("eq")
if field in model.__fields__:
compare_field = model.__fields__[field]
values = []
for raw_value in raw_values:
validated, errors = compare_field.validate(raw_value, {}, loc="none")
if errors:
raise ValidationError(errors=[errors], model=model)
values.append(validated)
else:
raise ValueError("Unknown filter field")
return cls(field=field, op=op, values=values, model=model)
@property
def statement(self):
if self.model and self.model.__fields__[self.field].type_ == datetime.datetime:
placeholder = compat_timestamp_placeholder()
else:
placeholder = "?"
if self.op in (Operator.INCLUDE, Operator.EXCLUDE):
placeholders = ", ".join([placeholder] * len(self.values))
stmt = [f"{self.field} {self.op.as_sql} ({placeholders})"]
else:
stmt = [f"{self.field} {self.op.as_sql} {placeholder}"] * len(self.values)
return " OR ".join(stmt)
class Filters(BaseModel, Generic[TFilterModel]):
"""
Generic helper class for filtering and sorting data.
For usage in an api endpoint, use the `parse_filters` dependency.
When constructing this class manually always make sure to pass a model so that
the values can be validated. Otherwise, make sure to validate the inputs manually.
"""
filters: List[Filter[TFilterModel]] = []
search: Optional[str] = None
offset: Optional[int] = None
limit: Optional[int] = None
sortby: Optional[str] = None
direction: Optional[Literal["asc", "desc"]] = None
model: Optional[Type[TFilterModel]] = None
@root_validator(pre=True)
def validate_sortby(cls, values):
sortby = values.get("sortby")
model = values.get("model")
if sortby and model:
model = values["model"]
# if no sort fields are specified explicitly all fields are allowed
allowed = model.__sort_fields__ or model.__fields__
if sortby not in allowed:
raise ValueError("Invalid sort field")
return values
def pagination(self) -> str:
stmt = ""
if self.limit:
stmt += f"LIMIT {self.limit} "
if self.offset:
stmt += f"OFFSET {self.offset}"
return stmt
def where(self, where_stmts: Optional[List[str]] = None) -> str:
if not where_stmts:
where_stmts = []
if self.filters:
for filter in self.filters:
where_stmts.append(filter.statement)
if self.search and self.model:
if DB_TYPE == POSTGRES:
where_stmts.append(
f"lower(concat({', '.join(self.model.__search_fields__)})) LIKE ?"
)
elif DB_TYPE == SQLITE:
where_stmts.append(
f"lower({'||'.join(self.model.__search_fields__)}) LIKE ?"
)
if where_stmts:
return "WHERE " + " AND ".join(where_stmts)
return ""
def order_by(self) -> str:
if self.sortby:
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
return ""
def values(self, values: Optional[List[str]] = None) -> tuple:
if not values:
values = []
if self.filters:
for filter in self.filters:
values.extend(filter.values)
if self.search and self.model:
values.append(f"%{self.search}%")
return tuple(values)
+85 -56
View File
@@ -1,40 +1,45 @@
from http import HTTPStatus
from typing import Literal, Optional, Type
from fastapi import Security, status
from fastapi import Query, Request, Security
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic.types import UUID4
from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.core.models import User, WalletType, WalletTypeInfo
from lnbits.db import Filter, Filters, TFilterModel
from lnbits.requestvars import g
from lnbits.settings import settings
# TODO: fix type ignores
class KeyChecker(SecurityBase):
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
self._key_type = "invoice"
self._key_type = WalletType.invoice
self._api_key = api_key
if api_key:
key = APIKey(
**{"in": APIKeyIn.query},
**{"in": APIKeyIn.query}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - QUERY",
)
else:
key = APIKey(
**{"in": APIKeyIn.header},
**{"in": APIKeyIn.header}, # type: ignore
name="X-API-KEY",
description="Wallet API Key - HEADER",
)
self.wallet = None # type: ignore
self.wallet = None
self.model: APIKey = key
async def __call__(self, request: Request):
@@ -44,16 +49,16 @@ class KeyChecker(SecurityBase):
if self._api_key
else request.headers.get("X-API-KEY") or request.query_params["api-key"]
)
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
# Also, we should not return the wallet here - thats silly.
# Possibly store it in a Redis DB
self.wallet = await get_wallet_for_key(key_value, self._key_type) # type: ignore
if not self.wallet:
# FIXME: Find another way to validate the key. A fetch from DB should be
# avoided here. Also, we should not return the wallet here - thats
# silly. Possibly store it in a Redis DB
wallet = await get_wallet_for_key(key_value, self._key_type)
if not wallet or wallet.deleted:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invalid key or expired key.",
detail="Invalid key or wallet.",
)
self.wallet = wallet # type: ignore
except KeyError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
@@ -70,10 +75,13 @@ class WalletInvoiceKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "invoice"
self._key_type = WalletType.invoice
class WalletAdminKeyChecker(KeyChecker):
@@ -86,19 +94,13 @@ class WalletAdminKeyChecker(KeyChecker):
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
self,
scheme_name: Optional[str] = None,
auto_error: bool = True,
api_key: Optional[str] = None,
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "admin"
class WalletTypeInfo:
wallet_type: int
wallet: Wallet
def __init__(self, wallet_type: int, wallet: Wallet) -> None:
self.wallet_type = wallet_type
self.wallet = wallet
self._key_type = WalletType.admin
api_key_header = APIKeyHeader(
@@ -118,11 +120,6 @@ async def get_key_type(
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
) -> WalletTypeInfo:
# 0: admin
# 1: invoice
# 2: invalid
pathname = r["path"].split("/")[1]
token = api_key_header or api_key_query
if not token:
@@ -131,38 +128,40 @@ async def get_key_type(
detail="Invoice (or Admin) key required.",
)
for typenr, WalletChecker in zip(
[0, 1], [WalletAdminKeyChecker, WalletInvoiceKeyChecker]
for wallet_type, WalletChecker in zip(
[WalletType.admin, WalletType.invoice],
[WalletAdminKeyChecker, WalletInvoiceKeyChecker],
):
try:
checker = WalletChecker(api_key=token)
await checker.__call__(r)
wallet = WalletTypeInfo(typenr, checker.wallet) # type: ignore
if wallet is None or wallet.wallet is None:
if checker.wallet is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
)
wallet = WalletTypeInfo(wallet_type, checker.wallet)
if (
wallet.wallet.user != settings.super_user
and wallet.wallet.user not in settings.lnbits_admin_users
) and (
settings.lnbits_admin_extensions
and pathname in settings.lnbits_admin_extensions
and r["path"].split("/")[1] in settings.lnbits_admin_extensions
):
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="User not authorized for this extension.",
)
return wallet
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
except HTTPException as exc:
if exc.status_code == HTTPStatus.BAD_REQUEST:
raise
elif e.status_code == HTTPStatus.UNAUTHORIZED:
# we pass this in case it is not an invoice key, nor an admin key, and then return NOT_FOUND at the end of this block
elif exc.status_code == HTTPStatus.UNAUTHORIZED:
# we pass this in case it is not an invoice key, nor an admin key,
# and then return NOT_FOUND at the end of this block
pass
else:
raise
except:
except Exception:
raise
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
@@ -174,7 +173,6 @@ async def require_admin_key(
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
if not token:
@@ -189,7 +187,7 @@ async def require_admin_key(
# If wallet type is not admin then return the unauthorized status
# This also covers when the user passes an invalid key type
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Admin key required."
status_code=HTTPStatus.UNAUTHORIZED, detail="Admin key required."
)
else:
return wallet
@@ -200,7 +198,6 @@ async def require_invoice_key(
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
token = api_key_header or api_key_query
if not token:
@@ -211,11 +208,12 @@ async def require_invoice_key(
wallet = await get_key_type(r, token)
if wallet.wallet_type > 1:
# If wallet type is not invoice then return the unauthorized status
# This also covers when the user passes an invalid key type
if (
wallet.wallet_type != WalletType.admin
and wallet.wallet_type != WalletType.invoice
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
else:
@@ -245,15 +243,11 @@ async def check_user_exists(usr: UUID4) -> User:
async def check_admin(usr: UUID4) -> User:
user = await check_user_exists(usr)
if user.id != settings.super_user and not user.id in settings.lnbits_admin_users:
if user.id != settings.super_user and user.id not in settings.lnbits_admin_users:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="User not authorized. No admin privileges.",
)
user.admin = True
user.super_user = False
if user.id == settings.super_user:
user.super_user = True
return user
@@ -266,3 +260,38 @@ async def check_super_user(usr: UUID4) -> User:
detail="User not authorized. No super user privileges.",
)
return user
def parse_filters(model: Type[TFilterModel]):
"""
Parses the query params as filters.
:param model: model used for validation of filter values
"""
def dependency(
request: Request,
limit: Optional[int] = None,
offset: Optional[int] = None,
sortby: Optional[str] = None,
direction: Optional[Literal["asc", "desc"]] = None,
search: Optional[str] = Query(None, description="Text based search"),
):
params = request.query_params
filters = []
for key in params.keys():
try:
filters.append(Filter.parse_query(key, params.getlist(key), model))
except ValueError:
continue
return Filters(
filters=filters,
limit=limit,
offset=offset,
sortby=sortby,
direction=direction,
search=search,
model=model,
)
return dependency
+615
View File
@@ -0,0 +1,615 @@
import hashlib
import json
import os
import shutil
import sys
import zipfile
from http import HTTPStatus
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Tuple
from urllib import request
import httpx
from fastapi import HTTPException
from loguru import logger
from packaging import version
from pydantic import BaseModel
from lnbits.settings import settings
class ExplicitRelease(BaseModel):
id: str
name: str
version: str
archive: str
hash: str
dependencies: List[str] = []
repo: Optional[str]
icon: Optional[str]
short_description: Optional[str]
min_lnbits_version: Optional[str]
html_url: Optional[str] # todo: release_url
warning: Optional[str]
info_notification: Optional[str]
critical_notification: Optional[str]
def is_version_compatible(self):
if not self.min_lnbits_version:
return True
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
class GitHubRelease(BaseModel):
id: str
organisation: str
repository: str
class Manifest(BaseModel):
featured: List[str] = []
extensions: List["ExplicitRelease"] = []
repos: List["GitHubRelease"] = []
class GitHubRepoRelease(BaseModel):
name: str
tag_name: str
zipball_url: str
html_url: str
class GitHubRepo(BaseModel):
stargazers_count: str
html_url: str
default_branch: str
class ExtensionConfig(BaseModel):
name: str
short_description: str
tile: str = ""
warning: Optional[str] = ""
min_lnbits_version: Optional[str]
def is_version_compatible(self):
if not self.min_lnbits_version:
return True
return version.parse(self.min_lnbits_version) <= version.parse(settings.version)
def download_url(url, save_path):
with request.urlopen(url) as dl_file:
with open(save_path, "wb") as out_file:
out_file.write(dl_file.read())
def file_hash(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
while n := f.readinto(mv):
h.update(mv[:n])
return h.hexdigest()
async def fetch_github_repo_info(
org: str, repository: str
) -> Tuple[GitHubRepo, GitHubRepoRelease, ExtensionConfig]:
repo_url = f"https://api.github.com/repos/{org}/{repository}"
error_msg = "Cannot fetch extension repo"
repo = await github_api_get(repo_url, error_msg)
github_repo = GitHubRepo.parse_obj(repo)
lates_release_url = (
f"https://api.github.com/repos/{org}/{repository}/releases/latest"
)
error_msg = "Cannot fetch extension releases"
latest_release: Any = await github_api_get(lates_release_url, error_msg)
config_url = f"https://raw.githubusercontent.com/{org}/{repository}/{github_repo.default_branch}/config.json"
error_msg = "Cannot fetch config for extension"
config = await github_api_get(config_url, error_msg)
return (
github_repo,
GitHubRepoRelease.parse_obj(latest_release),
ExtensionConfig.parse_obj(config),
)
async def fetch_manifest(url) -> Manifest:
error_msg = "Cannot fetch extensions manifest"
manifest = await github_api_get(url, error_msg)
return Manifest.parse_obj(manifest)
async def fetch_github_releases(org: str, repo: str) -> List[GitHubRepoRelease]:
releases_url = f"https://api.github.com/repos/{org}/{repo}/releases"
error_msg = "Cannot fetch extension releases"
releases = await github_api_get(releases_url, error_msg)
return [GitHubRepoRelease.parse_obj(r) for r in releases]
async def fetch_github_release_config(
org: str, repo: str, tag_name: str
) -> Optional[ExtensionConfig]:
config_url = (
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
)
error_msg = "Cannot fetch GitHub extension config"
config = await github_api_get(config_url, error_msg)
return ExtensionConfig.parse_obj(config)
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
async with httpx.AsyncClient() as client:
headers = (
{"Authorization": "Bearer " + settings.lnbits_ext_github_token}
if settings.lnbits_ext_github_token
else None
)
resp = await client.get(
url,
headers=headers,
)
if resp.status_code != 200:
logger.warning(f"{error_msg} ({url}): {resp.text}")
resp.raise_for_status()
return resp.json()
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
if not path:
return ""
_, _, *rest = path.split("/")
tail = "/".join(rest)
return f"https://github.com/{source_repo}/raw/main/{tail}"
class Extension(NamedTuple):
code: str
is_valid: bool
is_admin_only: bool
name: Optional[str] = None
short_description: Optional[str] = None
tile: Optional[str] = None
contributors: Optional[List[str]] = None
hidden: bool = False
migration_module: Optional[str] = None
db_name: Optional[str] = None
upgrade_hash: Optional[str] = ""
@property
def module_name(self) -> str:
if self.is_upgrade_extension:
if settings.has_default_extension_path:
return f"lnbits.upgrades.{self.code}-{self.upgrade_hash}"
return f"{self.code}-{self.upgrade_hash}"
if settings.has_default_extension_path:
return f"lnbits.extensions.{self.code}"
return self.code
@property
def is_upgrade_extension(self) -> bool:
return self.upgrade_hash != ""
@classmethod
def from_installable_ext(cls, ext_info: "InstallableExtension") -> "Extension":
return Extension(
code=ext_info.id,
is_valid=True,
is_admin_only=False, # todo: is admin only
name=ext_info.name,
upgrade_hash=ext_info.hash if ext_info.module_installed else "",
)
# All subdirectories in the current directory, not recursive.
class ExtensionManager:
def __init__(self) -> None:
p = Path(settings.lnbits_extensions_path, "extensions")
Path(p).mkdir(parents=True, exist_ok=True)
self._extension_folders: List[Path] = [f for f in p.iterdir() if f.is_dir()]
@property
def extensions(self) -> List[Extension]:
output: List[Extension] = []
for extension_folder in self._extension_folders:
extension_code = extension_folder.parts[-1]
try:
with open(extension_folder / "config.json") as json_file:
config = json.load(json_file)
is_valid = True
is_admin_only = extension_code in settings.lnbits_admin_extensions
except Exception:
config = {}
is_valid = False
is_admin_only = False
output.append(
Extension(
extension_code,
is_valid,
is_admin_only,
config.get("name"),
config.get("short_description"),
config.get("tile"),
config.get("contributors"),
config.get("hidden") or False,
config.get("migration_module"),
config.get("db_name"),
)
)
return output
class ExtensionRelease(BaseModel):
name: str
version: str
archive: str
source_repo: str
is_github_release: bool = False
hash: Optional[str] = None
min_lnbits_version: Optional[str] = None
is_version_compatible: Optional[bool] = True
html_url: Optional[str] = None
description: Optional[str] = None
warning: Optional[str] = None
repo: Optional[str] = None
icon: Optional[str] = None
@classmethod
def from_github_release(
cls, source_repo: str, r: "GitHubRepoRelease"
) -> "ExtensionRelease":
return ExtensionRelease(
name=r.name,
description=r.name,
version=r.tag_name,
archive=r.zipball_url,
source_repo=source_repo,
is_github_release=True,
repo=f"https://github.com/{source_repo}",
html_url=r.html_url,
)
@classmethod
def from_explicit_release(
cls, source_repo: str, e: "ExplicitRelease"
) -> "ExtensionRelease":
return ExtensionRelease(
name=e.name,
version=e.version,
archive=e.archive,
hash=e.hash,
source_repo=source_repo,
description=e.short_description,
min_lnbits_version=e.min_lnbits_version,
is_version_compatible=e.is_version_compatible(),
warning=e.warning,
html_url=e.html_url,
repo=e.repo,
icon=e.icon,
)
@classmethod
async def all_releases(cls, org: str, repo: str) -> List["ExtensionRelease"]:
try:
github_releases = await fetch_github_releases(org, repo)
return [
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
for r in github_releases
]
except Exception as e:
logger.warning(e)
return []
class InstallableExtension(BaseModel):
id: str
name: str
short_description: Optional[str] = None
icon: Optional[str] = None
dependencies: List[str] = []
is_admin_only: bool = False
stars: int = 0
featured = False
latest_release: Optional[ExtensionRelease] = None
installed_release: Optional[ExtensionRelease] = None
archive: Optional[str] = None
@property
def hash(self) -> str:
if self.installed_release:
if self.installed_release.hash:
return self.installed_release.hash
m = hashlib.sha256()
m.update(f"{self.installed_release.archive}".encode())
return m.hexdigest()
return "not-installed"
@property
def zip_path(self) -> Path:
extensions_data_dir = Path(settings.lnbits_data_folder, "zips")
Path(extensions_data_dir).mkdir(parents=True, exist_ok=True)
return Path(extensions_data_dir, f"{self.id}.zip")
@property
def ext_dir(self) -> Path:
return Path(settings.lnbits_extensions_path, "extensions", self.id)
@property
def ext_upgrade_dir(self) -> Path:
return Path(
settings.lnbits_extensions_path, "upgrades", f"{self.id}-{self.hash}"
)
@property
def module_name(self) -> str:
if settings.has_default_extension_path:
return f"lnbits.extensions.{self.id}"
return self.id
@property
def module_installed(self) -> bool:
return self.module_name in sys.modules
@property
def has_installed_version(self) -> bool:
if not self.ext_dir.is_dir():
return False
return Path(self.ext_dir, "config.json").is_file()
@property
def installed_version(self) -> str:
if self.installed_release:
return self.installed_release.version
return ""
def download_archive(self):
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
ext_zip_file = self.zip_path
if ext_zip_file.is_file():
os.remove(ext_zip_file)
try:
assert self.installed_release, "installed_release is none."
download_url(self.installed_release.archive, ext_zip_file)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Cannot fetch extension archive file",
)
archive_hash = file_hash(ext_zip_file)
if self.installed_release.hash and self.installed_release.hash != archive_hash:
# remove downloaded archive
if ext_zip_file.is_file():
os.remove(ext_zip_file)
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="File hash missmatch. Will not install.",
)
def extract_archive(self):
logger.info(f"Extracting extension {self.name} ({self.installed_version}).")
Path(settings.lnbits_extensions_path, "upgrades").mkdir(
parents=True, exist_ok=True
)
tmp_dir = Path(settings.lnbits_data_folder, "unzip-temp", self.hash)
shutil.rmtree(tmp_dir, True)
with zipfile.ZipFile(self.zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
generated_dir_name = os.listdir(tmp_dir)[0]
shutil.rmtree(self.ext_upgrade_dir, True)
shutil.copytree(
Path(tmp_dir, generated_dir_name),
Path(self.ext_upgrade_dir),
)
shutil.rmtree(tmp_dir, True)
# Pre-packed extensions can be upgraded
# Mark the extension as installed so we know it is not the pre-packed version
with open(Path(self.ext_upgrade_dir, "config.json"), "r+") as json_file:
config_json = json.load(json_file)
self.name = config_json.get("name")
self.short_description = config_json.get("short_description")
if (
self.installed_release
and self.installed_release.is_github_release
and config_json.get("tile")
):
self.icon = icon_to_github_url(
self.installed_release.source_repo, config_json.get("tile")
)
shutil.rmtree(self.ext_dir, True)
shutil.copytree(Path(self.ext_upgrade_dir), Path(self.ext_dir))
logger.success(f"Extension {self.name} ({self.installed_version}) installed.")
def nofiy_upgrade(self) -> None:
"""
Update the list of upgraded extensions. The middleware will perform
redirects based on this
"""
clean_upgraded_exts = list(
filter(
lambda old_ext: not old_ext.endswith(f"/{self.id}"),
settings.lnbits_upgraded_extensions,
)
)
settings.lnbits_upgraded_extensions = clean_upgraded_exts + [
f"{self.hash}/{self.id}"
]
def clean_extension_files(self):
# remove downloaded archive
if self.zip_path.is_file():
os.remove(self.zip_path)
# remove module from extensions
shutil.rmtree(self.ext_dir, True)
shutil.rmtree(self.ext_upgrade_dir, True)
def check_latest_version(self, release: Optional[ExtensionRelease]):
if not release:
return
if not self.latest_release:
self.latest_release = release
return
if version.parse(self.latest_release.version) < version.parse(release.version):
self.latest_release = release
@classmethod
def from_row(cls, data: dict) -> "InstallableExtension":
meta = json.loads(data["meta"])
ext = InstallableExtension(**data)
if "installed_release" in meta:
ext.installed_release = ExtensionRelease(**meta["installed_release"])
return ext
@classmethod
async def from_github_release(
cls, github_release: GitHubRelease
) -> Optional["InstallableExtension"]:
try:
repo, latest_release, config = await fetch_github_repo_info(
github_release.organisation, github_release.repository
)
return InstallableExtension(
id=github_release.id,
name=config.name,
short_description=config.short_description,
stars=int(repo.stargazers_count),
icon=icon_to_github_url(
f"{github_release.organisation}/{github_release.repository}",
config.tile,
),
latest_release=ExtensionRelease.from_github_release(
repo.html_url, latest_release
),
)
except Exception as e:
logger.warning(e)
return None
@classmethod
def from_explicit_release(cls, e: ExplicitRelease) -> "InstallableExtension":
return InstallableExtension(
id=e.id,
name=e.name,
archive=e.archive,
short_description=e.short_description,
icon=e.icon,
dependencies=e.dependencies,
)
@classmethod
async def get_installable_extensions(
cls,
) -> List["InstallableExtension"]:
extension_list: List[InstallableExtension] = []
extension_id_list: List[str] = []
for url in settings.lnbits_extensions_manifests:
try:
manifest = await fetch_manifest(url)
for r in manifest.repos:
ext = await InstallableExtension.from_github_release(r)
if not ext:
continue
existing_ext = next(
(ee for ee in extension_list if ee.id == r.id), None
)
if existing_ext:
existing_ext.check_latest_version(ext.latest_release)
continue
ext.featured = ext.id in manifest.featured
extension_list += [ext]
extension_id_list += [ext.id]
for e in manifest.extensions:
release = ExtensionRelease.from_explicit_release(url, e)
existing_ext = next(
(ee for ee in extension_list if ee.id == e.id), None
)
if existing_ext:
existing_ext.check_latest_version(release)
continue
ext = InstallableExtension.from_explicit_release(e)
ext.check_latest_version(release)
ext.featured = ext.id in manifest.featured
extension_list += [ext]
extension_id_list += [e.id]
except Exception as e:
logger.warning(f"Manifest {url} failed with '{str(e)}'")
return extension_list
@classmethod
async def get_extension_releases(cls, ext_id: str) -> List["ExtensionRelease"]:
extension_releases: List[ExtensionRelease] = []
for url in settings.lnbits_extensions_manifests:
try:
manifest = await fetch_manifest(url)
for r in manifest.repos:
if r.id == ext_id:
repo_releases = await ExtensionRelease.all_releases(
r.organisation, r.repository
)
extension_releases += repo_releases
for e in manifest.extensions:
if e.id == ext_id:
extension_releases += [
ExtensionRelease.from_explicit_release(url, e)
]
except Exception as e:
logger.warning(f"Manifest {url} failed with '{str(e)}'")
return extension_releases
@classmethod
async def get_extension_release(
cls, ext_id: str, source_repo: str, archive: str
) -> Optional["ExtensionRelease"]:
all_releases: List[
ExtensionRelease
] = await InstallableExtension.get_extension_releases(ext_id)
selected_release = [
r
for r in all_releases
if r.archive == archive and r.source_repo == source_repo
]
return selected_release[0] if len(selected_release) != 0 else None
class CreateExtension(BaseModel):
ext_id: str
archive: str
source_repo: str
def get_valid_extensions() -> List[Extension]:
return [
extension for extension in ExtensionManager().extensions if extension.is_valid
]
-21
View File
@@ -1,21 +0,0 @@
# Bleskomat Extension for lnbits
This extension allows you to connect a Bleskomat ATM to an lnbits wallet. It will work with both the [open-source DIY Bleskomat ATM project](https://github.com/samotari/bleskomat) as well as the [commercial Bleskomat ATM](https://www.bleskomat.com/).
## Connect Your Bleskomat ATM
* Click the "Add Bleskomat" button on this page to begin.
* Choose a wallet. This will be the wallet that is used to pay satoshis to your ATM customers.
* Choose the fiat currency. This should match the fiat currency that your ATM accepts.
* Pick an exchange rate provider. This is the API that will be used to query the fiat to satoshi exchange rate at the time your customer attempts to withdraw their funds.
* Set your ATM's fee percentage.
* Click the "Done" button.
* Find the new Bleskomat in the list and then click the export icon to download a new configuration file for your ATM.
* Copy the configuration file ("bleskomat.conf") to your ATM's SD card.
* Restart Your Bleskomat ATM. It should automatically reload the configurations from the SD card.
## How Does It Work?
Since the Bleskomat ATMs are designed to be offline, a cryptographic signing scheme is used to verify that the URL was generated by an authorized device. When one of your customers inserts fiat money into the device, a signed URL (lnurl-withdraw) is created and displayed as a QR code. Your customer scans the QR code with their lnurl-supporting mobile app, their mobile app communicates with the web API of lnbits to verify the signature, the fiat currency amount is converted to sats, the customer accepts the withdrawal, and finally lnbits will pay the customer from your lnbits wallet.
-26
View File
@@ -1,26 +0,0 @@
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_bleskomat")
bleskomat_static_files = [
{
"path": "/bleskomat/static",
"app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
"name": "bleskomat_static",
}
]
bleskomat_ext: APIRouter = APIRouter(prefix="/bleskomat", tags=["Bleskomat"])
def bleskomat_renderer():
return template_renderer(["lnbits/extensions/bleskomat/templates"])
from .lnurl_api import * # noqa
from .views import * # noqa
from .views_api import * # noqa
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Bleskomat",
"short_description": "Connect a Bleskomat ATM to an lnbits",
"tile": "/bleskomat/static/image/bleskomat.png",
"contributors": ["chill117"]
}
-113
View File
@@ -1,113 +0,0 @@
import secrets
import time
from typing import List, Optional, Union
from uuid import uuid4
from . import db
from .helpers import generate_bleskomat_lnurl_hash
from .models import Bleskomat, BleskomatLnurl, CreateBleskomat
async def create_bleskomat(data: CreateBleskomat, wallet_id: str) -> Bleskomat:
bleskomat_id = uuid4().hex
api_key_id = secrets.token_hex(8)
api_key_secret = secrets.token_hex(32)
api_key_encoding = "hex"
await db.execute(
"""
INSERT INTO bleskomat.bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
bleskomat_id,
wallet_id,
api_key_id,
api_key_secret,
api_key_encoding,
data.name,
data.fiat_currency,
data.exchange_rate_provider,
data.fee,
),
)
bleskomat = await get_bleskomat(bleskomat_id)
assert bleskomat, "Newly created bleskomat couldn't be retrieved"
return bleskomat
async def get_bleskomat(bleskomat_id: str) -> Optional[Bleskomat]:
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
)
return Bleskomat(**row) if row else None
async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]:
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomats WHERE api_key_id = ?", (api_key_id,)
)
return Bleskomat(**row) if row else None
async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM bleskomat.bleskomats WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Bleskomat(**row) for row in rows]
async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE bleskomat.bleskomats SET {q} WHERE id = ?",
(*kwargs.values(), bleskomat_id),
)
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
)
return Bleskomat(**row) if row else None
async def delete_bleskomat(bleskomat_id: str) -> None:
await db.execute("DELETE FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,))
async def create_bleskomat_lnurl(
*, bleskomat: Bleskomat, secret: str, tag: str, params: str, uses: int = 1
) -> BleskomatLnurl:
bleskomat_lnurl_id = uuid4().hex
hash = generate_bleskomat_lnurl_hash(secret)
now = int(time.time())
await db.execute(
"""
INSERT INTO bleskomat.bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
bleskomat_lnurl_id,
bleskomat.id,
bleskomat.wallet,
hash,
tag,
params,
bleskomat.api_key_id,
uses,
uses,
now,
now,
),
)
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl, "Newly created bleskomat LNURL couldn't be retrieved"
return bleskomat_lnurl
async def get_bleskomat_lnurl(secret: str) -> Optional[BleskomatLnurl]:
hash = generate_bleskomat_lnurl_hash(secret)
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomat_lnurls WHERE hash = ?", (hash,)
)
return BleskomatLnurl(**row) if row else None
@@ -1,86 +0,0 @@
import json
import os
from typing import Callable, Dict, Union
import httpx
fiat_currencies = json.load(
open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)), "fiat_currencies.json"
),
"r",
)
)
exchange_rate_providers: dict[
str, dict[str, Union[str, Callable[[dict, dict], str]]]
] = {
"bitfinex": {
"name": "Bitfinex",
"domain": "bitfinex.com",
"api_url": "https://api.bitfinex.com/v1/pubticker/{from}{to}",
"getter": lambda data, replacements: data["last_price"],
},
"bitstamp": {
"name": "Bitstamp",
"domain": "bitstamp.net",
"api_url": "https://www.bitstamp.net/api/v2/ticker/{from}{to}/",
"getter": lambda data, replacements: data["last"],
},
"coinbase": {
"name": "Coinbase",
"domain": "coinbase.com",
"api_url": "https://api.coinbase.com/v2/exchange-rates?currency={FROM}",
"getter": lambda data, replacements: data["data"]["rates"][replacements["TO"]],
},
"coinmate": {
"name": "CoinMate",
"domain": "coinmate.io",
"api_url": "https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}",
"getter": lambda data, replacements: data["data"]["last"],
},
"kraken": {
"name": "Kraken",
"domain": "kraken.com",
"api_url": "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}",
"getter": lambda data, replacements: data["result"][
"XXBTZ" + replacements["TO"]
]["c"][0],
},
}
exchange_rate_providers_serializable = {}
for ref, exchange_rate_provider in exchange_rate_providers.items():
exchange_rate_provider_serializable = {}
for key, value in exchange_rate_provider.items():
if not callable(value):
exchange_rate_provider_serializable[key] = value
exchange_rate_providers_serializable[ref] = exchange_rate_provider_serializable
async def fetch_fiat_exchange_rate(currency: str, provider: str):
replacements = {
"FROM": "BTC",
"from": "btc",
"TO": currency.upper(),
"to": currency.lower(),
}
api_url_or_none = exchange_rate_providers[provider]["api_url"]
if api_url_or_none is not None:
api_url = str(api_url_or_none)
for key in replacements.keys():
api_url = api_url.replace("{" + key + "}", replacements[key])
async with httpx.AsyncClient() as client:
r = await client.get(api_url)
r.raise_for_status()
data = r.json()
else:
data = {}
getter = exchange_rate_providers[provider]["getter"]
print(getter)
if callable(getter):
rate = float(getter(data, replacements))
return rate
@@ -1,166 +0,0 @@
{
"AED": "United Arab Emirates Dirham",
"AFN": "Afghan Afghani",
"ALL": "Albanian Lek",
"AMD": "Armenian Dram",
"ANG": "Netherlands Antillean Gulden",
"AOA": "Angolan Kwanza",
"ARS": "Argentine Peso",
"AUD": "Australian Dollar",
"AWG": "Aruban Florin",
"AZN": "Azerbaijani Manat",
"BAM": "Bosnia and Herzegovina Convertible Mark",
"BBD": "Barbadian Dollar",
"BDT": "Bangladeshi Taka",
"BGN": "Bulgarian Lev",
"BHD": "Bahraini Dinar",
"BIF": "Burundian Franc",
"BMD": "Bermudian Dollar",
"BND": "Brunei Dollar",
"BOB": "Bolivian Boliviano",
"BRL": "Brazilian Real",
"BSD": "Bahamian Dollar",
"BTN": "Bhutanese Ngultrum",
"BWP": "Botswana Pula",
"BYN": "Belarusian Ruble",
"BYR": "Belarusian Ruble",
"BZD": "Belize Dollar",
"CAD": "Canadian Dollar",
"CDF": "Congolese Franc",
"CHF": "Swiss Franc",
"CLF": "Unidad de Fomento",
"CLP": "Chilean Peso",
"CNH": "Chinese Renminbi Yuan Offshore",
"CNY": "Chinese Renminbi Yuan",
"COP": "Colombian Peso",
"CRC": "Costa Rican Colón",
"CUC": "Cuban Convertible Peso",
"CVE": "Cape Verdean Escudo",
"CZK": "Czech Koruna",
"DJF": "Djiboutian Franc",
"DKK": "Danish Krone",
"DOP": "Dominican Peso",
"DZD": "Algerian Dinar",
"EGP": "Egyptian Pound",
"ERN": "Eritrean Nakfa",
"ETB": "Ethiopian Birr",
"EUR": "Euro",
"FJD": "Fijian Dollar",
"FKP": "Falkland Pound",
"GBP": "British Pound",
"GEL": "Georgian Lari",
"GGP": "Guernsey Pound",
"GHS": "Ghanaian Cedi",
"GIP": "Gibraltar Pound",
"GMD": "Gambian Dalasi",
"GNF": "Guinean Franc",
"GTQ": "Guatemalan Quetzal",
"GYD": "Guyanese Dollar",
"HKD": "Hong Kong Dollar",
"HNL": "Honduran Lempira",
"HRK": "Croatian Kuna",
"HTG": "Haitian Gourde",
"HUF": "Hungarian Forint",
"IDR": "Indonesian Rupiah",
"ILS": "Israeli New Sheqel",
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
"JOD": "Jordanian Dinar",
"JPY": "Japanese Yen",
"KES": "Kenyan Shilling",
"KGS": "Kyrgyzstani Som",
"KHR": "Cambodian Riel",
"KMF": "Comorian Franc",
"KRW": "South Korean Won",
"KWD": "Kuwaiti Dinar",
"KYD": "Cayman Islands Dollar",
"KZT": "Kazakhstani Tenge",
"LAK": "Lao Kip",
"LBP": "Lebanese Pound",
"LKR": "Sri Lankan Rupee",
"LRD": "Liberian Dollar",
"LSL": "Lesotho Loti",
"LYD": "Libyan Dinar",
"MAD": "Moroccan Dirham",
"MDL": "Moldovan Leu",
"MGA": "Malagasy Ariary",
"MKD": "Macedonian Denar",
"MMK": "Myanmar Kyat",
"MNT": "Mongolian Tögrög",
"MOP": "Macanese Pataca",
"MRO": "Mauritanian Ouguiya",
"MUR": "Mauritian Rupee",
"MVR": "Maldivian Rufiyaa",
"MWK": "Malawian Kwacha",
"MXN": "Mexican Peso",
"MYR": "Malaysian Ringgit",
"MZN": "Mozambican Metical",
"NAD": "Namibian Dollar",
"NGN": "Nigerian Naira",
"NIO": "Nicaraguan Córdoba",
"NOK": "Norwegian Krone",
"NPR": "Nepalese Rupee",
"NZD": "New Zealand Dollar",
"OMR": "Omani Rial",
"PAB": "Panamanian Balboa",
"PEN": "Peruvian Sol",
"PGK": "Papua New Guinean Kina",
"PHP": "Philippine Peso",
"PKR": "Pakistani Rupee",
"PLN": "Polish Złoty",
"PYG": "Paraguayan Guaraní",
"QAR": "Qatari Riyal",
"RON": "Romanian Leu",
"RSD": "Serbian Dinar",
"RUB": "Russian Ruble",
"RWF": "Rwandan Franc",
"SAR": "Saudi Riyal",
"SBD": "Solomon Islands Dollar",
"SCR": "Seychellois Rupee",
"SEK": "Swedish Krona",
"SGD": "Singapore Dollar",
"SHP": "Saint Helenian Pound",
"SLL": "Sierra Leonean Leone",
"SOS": "Somali Shilling",
"SRD": "Surinamese Dollar",
"SSP": "South Sudanese Pound",
"STD": "São Tomé and Príncipe Dobra",
"SVC": "Salvadoran Colón",
"SZL": "Swazi Lilangeni",
"THB": "Thai Baht",
"TJS": "Tajikistani Somoni",
"TMT": "Turkmenistani Manat",
"TND": "Tunisian Dinar",
"TOP": "Tongan Paʻanga",
"TRY": "Turkish Lira",
"TTD": "Trinidad and Tobago Dollar",
"TWD": "New Taiwan Dollar",
"TZS": "Tanzanian Shilling",
"UAH": "Ukrainian Hryvnia",
"UGX": "Ugandan Shilling",
"USD": "US Dollar",
"UYU": "Uruguayan Peso",
"UZS": "Uzbekistan Som",
"VEF": "Venezuelan Bolívar",
"VES": "Venezuelan Bolívar Soberano",
"VND": "Vietnamese Đồng",
"VUV": "Vanuatu Vatu",
"WST": "Samoan Tala",
"XAF": "Central African Cfa Franc",
"XAG": "Silver (Troy Ounce)",
"XAU": "Gold (Troy Ounce)",
"XCD": "East Caribbean Dollar",
"XDR": "Special Drawing Rights",
"XOF": "West African Cfa Franc",
"XPD": "Palladium",
"XPF": "Cfp Franc",
"XPT": "Platinum",
"YER": "Yemeni Rial",
"ZAR": "South African Rand",
"ZMW": "Zambian Kwacha",
"ZWL": "Zimbabwean Dollar"
}
-153
View File
@@ -1,153 +0,0 @@
import base64
import hashlib
import hmac
from http import HTTPStatus
from typing import Dict
from urllib import parse
from fastapi import Request
def generate_bleskomat_lnurl_hash(secret: str):
m = hashlib.sha256()
m.update(f"{secret}".encode())
return m.hexdigest()
def generate_bleskomat_lnurl_signature(
payload: str, api_key_secret: str, api_key_encoding: str = "hex"
):
if api_key_encoding == "hex":
key = bytes.fromhex(api_key_secret)
elif api_key_encoding == "base64":
key = base64.b64decode(api_key_secret)
else:
key = bytes.fromhex(api_key_secret)
return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
def generate_bleskomat_lnurl_secret(api_key_id: str, signature: str):
# The secret is not randomly generated by the server.
# Instead it is the hash of the API key ID and signature concatenated together.
m = hashlib.sha256()
m.update(f"{api_key_id}-{signature}".encode())
return m.hexdigest()
def get_callback_url(req: Request):
return req.url_for("bleskomat.api_bleskomat_lnurl")
def is_supported_lnurl_subprotocol(tag: str) -> bool:
return tag == "withdrawRequest"
class LnurlHttpError(Exception):
def __init__(
self,
message: str = "",
http_status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
):
self.message = message
self.http_status = http_status
super().__init__(self.message)
class LnurlValidationError(Exception):
pass
def prepare_lnurl_params(tag: str, query: dict) -> dict:
params: dict = {}
if not is_supported_lnurl_subprotocol(tag):
raise LnurlValidationError(f'Unsupported subprotocol: "{tag}"')
if tag == "withdrawRequest":
params["minWithdrawable"] = float(query["minWithdrawable"])
params["maxWithdrawable"] = float(query["maxWithdrawable"])
params["defaultDescription"] = query["defaultDescription"]
if not params["minWithdrawable"] > 0:
raise LnurlValidationError('"minWithdrawable" must be greater than zero')
if not params["maxWithdrawable"] >= params["minWithdrawable"]:
raise LnurlValidationError(
'"maxWithdrawable" must be greater than or equal to "minWithdrawable"'
)
return params
encode_uri_component_safe_chars = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()"
)
def query_to_signing_payload(query: Dict[str, str]) -> str:
# Sort the query by key, then stringify it to create the payload.
sorted_keys = sorted(query.keys(), key=str.lower)
payload = []
for key in sorted_keys:
if not key == "signature":
encoded_key = parse.quote(key, safe=encode_uri_component_safe_chars)
encoded_value = parse.quote(
query[key], safe=encode_uri_component_safe_chars
)
payload.append(f"{encoded_key}={encoded_value}")
return "&".join(payload)
unshorten_rules: dict[str, dict] = {
"query": {"n": "nonce", "s": "signature", "t": "tag"},
"tags": {
"c": "channelRequest",
"l": "login",
"p": "payRequest",
"w": "withdrawRequest",
},
"params": {
"channelRequest": {"pl": "localAmt", "pp": "pushAmt"},
"login": {},
"payRequest": {"pn": "minSendable", "px": "maxSendable", "pm": "metadata"},
"withdrawRequest": {
"pn": "minWithdrawable",
"px": "maxWithdrawable",
"pd": "defaultDescription",
},
},
}
def unshorten_lnurl_query(query: dict) -> Dict[str, str]:
new_query = {}
rules = unshorten_rules
if "tag" in query:
tag = query["tag"]
elif "t" in query:
tag = query["t"]
else:
raise LnurlValidationError('Missing required query parameter: "tag"')
# Unshorten tag:
if tag in rules["tags"]:
long_tag = rules["tags"][tag]
new_query["tag"] = long_tag
tag = long_tag
if not tag in rules["params"]:
raise LnurlValidationError(f'Unknown tag: "{tag}"')
for key in query:
if key in rules["params"][str(tag)]:
short_param_key = key
long_param_key = rules["params"][str(tag)][short_param_key]
if short_param_key in query:
new_query[long_param_key] = query[short_param_key]
else:
new_query[long_param_key] = query[long_param_key]
elif key in rules["query"]:
# Unshorten general keys:
short_key = key
long_key = rules["query"][short_key]
if not long_key in new_query:
if short_key in query:
new_query[long_key] = query[short_key]
else:
new_query[long_key] = query[str(long_key)]
else:
# Keep unknown key/value pairs unchanged:
new_query[key] = query[key]
return new_query
-132
View File
@@ -1,132 +0,0 @@
import json
import math
from http import HTTPStatus
from loguru import logger
from starlette.requests import Request
from . import bleskomat_ext
from .crud import (
create_bleskomat_lnurl,
get_bleskomat_by_api_key_id,
get_bleskomat_lnurl,
)
from .exchange_rates import fetch_fiat_exchange_rate
from .helpers import (
LnurlHttpError,
LnurlValidationError,
generate_bleskomat_lnurl_secret,
generate_bleskomat_lnurl_signature,
prepare_lnurl_params,
query_to_signing_payload,
unshorten_lnurl_query,
)
# Handles signed URL from Bleskomat ATMs and "action" callback of auto-generated LNURLs.
@bleskomat_ext.get("/u", name="bleskomat.api_bleskomat_lnurl")
async def api_bleskomat_lnurl(req: Request):
try:
query = dict(req.query_params)
# Unshorten query if "s" is used instead of "signature".
if "s" in query:
query = unshorten_lnurl_query(query)
if "signature" in query:
# Signature provided.
# Use signature to verify that the URL was generated by an authorized device.
# Later validate parameters, auto-generate LNURL, reply with LNURL response object.
signature = query["signature"]
# The API key ID, nonce, and tag should be present in the query string.
for field in ["id", "nonce", "tag"]:
if not field in query:
raise LnurlHttpError(
f'Failed API key signature check: Missing "{field}"',
HTTPStatus.BAD_REQUEST,
)
# URL signing scheme is described here:
# https://github.com/chill117/lnurl-node#how-to-implement-url-signing-scheme
payload = query_to_signing_payload(query)
api_key_id = query["id"]
bleskomat = await get_bleskomat_by_api_key_id(api_key_id)
if not bleskomat:
raise LnurlHttpError("Unknown API key", HTTPStatus.BAD_REQUEST)
api_key_secret = bleskomat.api_key_secret
api_key_encoding = bleskomat.api_key_encoding
expected_signature = generate_bleskomat_lnurl_signature(
payload, api_key_secret, api_key_encoding
)
if signature != expected_signature:
raise LnurlHttpError("Invalid API key signature", HTTPStatus.FORBIDDEN)
# Signature is valid.
# In the case of signed URLs, the secret is deterministic based on the API key ID and signature.
secret = generate_bleskomat_lnurl_secret(api_key_id, signature)
lnurl = await get_bleskomat_lnurl(secret)
if not lnurl:
try:
tag = query["tag"]
params = prepare_lnurl_params(tag, query)
if "f" in query:
rate = await fetch_fiat_exchange_rate(
currency=query["f"],
provider=bleskomat.exchange_rate_provider,
)
# Convert fee (%) to decimal:
fee = float(bleskomat.fee) / 100
if tag == "withdrawRequest":
for key in ["minWithdrawable", "maxWithdrawable"]:
amount_sats = int(
math.floor((params[key] / rate) * 1e8)
)
fee_sats = int(math.floor(amount_sats * fee))
amount_sats_less_fee = amount_sats - fee_sats
# Convert to msats:
params[key] = int(amount_sats_less_fee * 1e3)
except LnurlValidationError as e:
raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
# Create a new LNURL using the query parameters provided in the signed URL.
json_params = json.JSONEncoder().encode(params)
lnurl = await create_bleskomat_lnurl(
bleskomat=bleskomat,
secret=secret,
tag=tag,
params=json_params,
uses=1,
)
# Reply with LNURL response object.
return lnurl.get_info_response_object(secret, req)
# No signature provided.
# Treat as "action" callback.
if not "k1" in query:
raise LnurlHttpError("Missing secret", HTTPStatus.BAD_REQUEST)
secret = query["k1"]
lnurl = await get_bleskomat_lnurl(secret)
if not lnurl:
raise LnurlHttpError("Invalid secret", HTTPStatus.BAD_REQUEST)
if not lnurl.has_uses_remaining():
raise LnurlHttpError(
"Maximum number of uses already reached", HTTPStatus.BAD_REQUEST
)
try:
await lnurl.execute_action(query)
except LnurlValidationError as e:
raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
except LnurlHttpError as e:
return {"status": "ERROR", "reason": str(e)}
except Exception as e:
logger.error(str(e))
return {"status": "ERROR", "reason": "Unexpected error"}
return {"status": "OK"}
-37
View File
@@ -1,37 +0,0 @@
async def m001_initial(db):
await db.execute(
"""
CREATE TABLE bleskomat.bleskomats (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
api_key_id TEXT NOT NULL,
api_key_secret TEXT NOT NULL,
api_key_encoding TEXT NOT NULL,
name TEXT NOT NULL,
fiat_currency TEXT NOT NULL,
exchange_rate_provider TEXT NOT NULL,
fee TEXT NOT NULL,
UNIQUE(api_key_id)
);
"""
)
await db.execute(
"""
CREATE TABLE bleskomat.bleskomat_lnurls (
id TEXT PRIMARY KEY,
bleskomat TEXT NOT NULL,
wallet TEXT NOT NULL,
hash TEXT NOT NULL,
tag TEXT NOT NULL,
params TEXT NOT NULL,
api_key_id TEXT NOT NULL,
initial_uses INTEGER DEFAULT 1,
remaining_uses INTEGER DEFAULT 0,
created_time INTEGER,
updated_time INTEGER,
UNIQUE(hash)
);
"""
)
-142
View File
@@ -1,142 +0,0 @@
import json
import time
from typing import Dict
from fastapi import Query, Request
from loguru import logger
from pydantic import BaseModel, validator
from lnbits import bolt11
from lnbits.core.services import PaymentFailure, pay_invoice
from . import db
from .exchange_rates import exchange_rate_providers, fiat_currencies
from .helpers import LnurlValidationError, get_callback_url
class CreateBleskomat(BaseModel):
name: str = Query(...)
fiat_currency: str = Query(...)
exchange_rate_provider: str = Query(...)
fee: str = Query(...)
@validator("fiat_currency")
def allowed_fiat_currencies(cls, v):
if v not in fiat_currencies.keys():
raise ValueError("Not allowed currency")
return v
@validator("exchange_rate_provider")
def allowed_providers(cls, v):
if v not in exchange_rate_providers.keys():
raise ValueError("Not allowed provider")
return v
@validator("fee")
def fee_type(cls, v):
if not isinstance(v, (str, float, int)):
raise ValueError("Fee type not allowed")
return v
class Bleskomat(BaseModel):
id: str
wallet: str
api_key_id: str
api_key_secret: str
api_key_encoding: str
name: str
fiat_currency: str
exchange_rate_provider: str
fee: str
class BleskomatLnurl(BaseModel):
id: str
bleskomat: str
wallet: str
hash: str
tag: str
params: str
api_key_id: str
initial_uses: int
remaining_uses: int
created_time: int
updated_time: int
def has_uses_remaining(self) -> bool:
# When initial uses is 0 then the LNURL has unlimited uses.
return self.initial_uses == 0 or self.remaining_uses > 0
def get_info_response_object(self, secret: str, req: Request) -> Dict[str, str]:
tag = self.tag
params = json.loads(self.params)
response = {"tag": tag}
if tag == "withdrawRequest":
for key in ["minWithdrawable", "maxWithdrawable", "defaultDescription"]:
response[key] = params[key]
response["callback"] = get_callback_url(req)
response["k1"] = secret
return response
def validate_action(self, query) -> None:
tag = self.tag
params = json.loads(self.params)
# Perform tag-specific checks.
if tag == "withdrawRequest":
for field in ["pr"]:
if not field in query:
raise LnurlValidationError(f'Missing required parameter: "{field}"')
# Check the bolt11 invoice(s) provided.
pr = query["pr"]
if "," in pr:
raise LnurlValidationError("Multiple payment requests not supported")
try:
invoice = bolt11.decode(pr)
except ValueError:
raise LnurlValidationError(
'Invalid parameter ("pr"): Lightning payment request expected'
)
if invoice.amount_msat < params["minWithdrawable"]:
raise LnurlValidationError(
'Amount in invoice must be greater than or equal to "minWithdrawable"'
)
if invoice.amount_msat > params["maxWithdrawable"]:
raise LnurlValidationError(
'Amount in invoice must be less than or equal to "maxWithdrawable"'
)
else:
raise LnurlValidationError(f'Unknown subprotocol: "{tag}"')
async def execute_action(self, query):
self.validate_action(query)
used = False
async with db.connect() as conn:
if self.initial_uses > 0:
used = await self.use(conn)
if not used:
raise LnurlValidationError("Maximum number of uses already reached")
tag = self.tag
if tag == "withdrawRequest":
try:
await pay_invoice(
wallet_id=self.wallet, payment_request=query["pr"]
)
except (ValueError, PermissionError, PaymentFailure) as e:
raise LnurlValidationError("Failed to pay invoice: " + str(e))
except Exception as e:
logger.error(str(e))
raise LnurlValidationError("Unexpected error")
async def use(self, conn) -> bool:
now = int(time.time())
result = await conn.execute(
"""
UPDATE bleskomat.bleskomat_lnurls
SET remaining_uses = remaining_uses - 1, updated_time = ?
WHERE id = ?
AND remaining_uses > 0
""",
(now, self.id),
)
return result.rowcount > 0

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