Compare commits

...
416 Commits
Author SHA1 Message Date
ArcandGitHub f973de2f22 Merge pull request #790 from lnbits/addswagger
Adds link to swagger docs in all extensions
2022-07-27 19:40:22 +01:00
ArcandGitHub c3d3f843c1 Merge pull request #789 from lnbits/fastapimeta
Added meta for api /docs
2022-07-27 19:39:53 +01:00
ben 64ea5db608 disabled lnurlpayout again 2022-07-27 19:36:32 +01:00
ben 3a28ff1020 added swagger to extensions 2022-07-27 19:21:04 +01:00
ben 58659d2f47 Added meta for api /docs 2022-07-27 18:20:36 +01:00
ArcandGitHub bb002c6d40 Update wallets.md 2022-07-27 17:57:59 +01:00
ArcandGitHub 22592d6fde Merge pull request #788 from lnbits/eventsfix
Made ticket scanning reliable
2022-07-27 17:34:59 +01:00
ben 1c77d165e2 Made reliable
Fixed issue scanning tickets by adding "ticket://" prefix
2022-07-27 17:32:03 +01:00
Tiago VasconcelosandGitHub 41dc1b0512 Fix/hotfix tpos 2 (#783)
* fix older tpos without tips

* bad math requesting invoice

* make format
2022-07-27 16:45:17 +02:00
ArcandGitHub 8cac577f2f Update installation.md 2022-07-27 15:09:41 +01:00
ArcandGitHub 2b811ec1bb Update installation.md 2022-07-27 15:07:16 +01:00
ArcandGitHub ee1cc28187 Update installation.md 2022-07-27 15:01:23 +01:00
ArcandGitHub 1389098477 Merge pull request #721 from MatthewCroughan/matthewcroughan/nixify
Add Nix and Poetry to the project
2022-07-27 14:54:27 +01:00
ben 6e344561e3 Updated requirements 2022-07-27 14:49:13 +01:00
calleandGitHub 0c5cb8cb9e Wallets: increase timeouts (#787)
* increase pay timeouts to infinite
2022-07-27 15:29:44 +02:00
ben b1083a895e Merge branch 'main' into matthewcroughan/nixify 2022-07-27 13:50:05 +01:00
ben f6c38f2556 Merge branch 'matthewcroughan/nixify' into main 2022-07-27 13:48:42 +01:00
ben b90b5ea517 Merge branch 'main' into matthewcroughan/nixify 2022-07-25 15:59:54 +01:00
ArcandGitHub ea76995467 Merge pull request #782 from lnbits/revert-781-revert-758-lnurl/check_description_hash
Revert "Revert "LNURLp: check description hash""
2022-07-25 15:55:05 +01:00
ArcandGitHub 2d46eda5f5 Revert "Revert "LNURLp: check description hash"" 2022-07-25 15:54:49 +01:00
ben 309a4dbd1a Merge branch 'main' into matthewcroughan/nixify 2022-07-25 15:33:41 +01:00
ArcandGitHub 2834ce0b4f Merge pull request #781 from lnbits/revert-758-lnurl/check_description_hash
Revert "LNURLp: check description hash"
2022-07-25 15:33:11 +01:00
ArcandGitHub b708950b93 Revert "LNURLp: check description hash" 2022-07-25 15:32:59 +01:00
ben c7e5d66801 Merge branch 'main' into matthewcroughan/nixify 2022-07-25 15:20:17 +01:00
ArcandGitHub 1da7c9b349 Merge pull request #758 from lnbits/lnurl/check_description_hash
LNURLp: check description hash
2022-07-25 15:19:20 +01:00
ben 3ffcab2d4d Merge branch 'main' into matthewcroughan/nixify 2022-07-25 15:01:33 +01:00
462fffbeaa .gitignore coverage.xml + do not delete mock_data.zip (#779)
* add coverage.xml to gitignore

* fix deleting mock_data.zip

Co-authored-by: dni <dni.khr@gmail.com>
2022-07-25 15:08:30 +02:00
calleandGitHub 210edde08a Migration: Fix withdraw (#780) 2022-07-25 15:04:55 +02:00
Tiago VasconcelosandGitHub 27b4db9877 fix older tpos without tips (#778) 2022-07-25 12:52:28 +02:00
ben 5f3183c54c Merge branch 'main' into matthewcroughan/nixify 2022-07-25 10:34:29 +01:00
Tiago VasconcelosandGitHub 7fbea79e62 Add voucher design (#704)
* allow for custom print of vouchers

* lnbits defaul voucher

* reduce png size

* add template for custom design

* rename lnbits voucher

* send voucher amount to UI

* add some tweaks for default voucher

* added bonus in readme

* blacked

* make default string as constant
2022-07-25 11:22:21 +02:00
Tiago VasconcelosandGitHub ff5b1189b5 Fix/tpos tips #697 (#713)
* small fastapi convertion

* make tip add up to invoice

* make tip add to invoice

* display existing tpos

* clean console logs

* suggestion from @leesalminen

* blacked
2022-07-25 11:19:03 +02:00
calleandGitHub 4bb184e08b CI: DB migration with real data (#776)
* fix migration conv.py

* migrate with mock data
2022-07-25 09:13:41 +02:00
4b5e113c03 Regtest update (#775)
* preparation pr for regtest update

* remove FIXMEs

Co-authored-by: dni <dni.khr@gmail.com>
2022-07-25 07:31:15 +02:00
ben 244d6f23bf Merge branch 'main' into matthewcroughan/nixify 2022-07-23 20:09:24 +01:00
ben 0e8da3c368 Fixed path for extension templates 2022-07-23 20:08:59 +01:00
calleandGitHub 32a8736f32 CI: Test core/views/generic.py enabling and disabling extensions (#774) 2022-07-23 11:58:29 +02:00
calleandGitHub bf2a3e9df2 fix tip link (#773) 2022-07-23 11:01:23 +02:00
blackcoffeexbtandGitHub c0c26fb98e Extension list UI improvements (#769)
* Extensions list is now sorted alphabetically

* Added extension list search

* Prettified changes

* Removed console.log

* Code improvements based on motorina0 feedback

* Remove console.log from lnbits/templates/base.html

Run prettier
2022-07-23 10:46:30 +02:00
calleandGitHub 96af5fc3a7 CI: Test core/views/generic.py (#772)
* Adds tests for GET /wallet

* Update `httpx` to `0.23.0` and `http-core` to `0.15.0` in `venv` installation path

* Fix `secp256k1 = "==0.14.0"` and `cffi = "==1.15.0"`
2022-07-23 10:39:58 +02:00
calleandGitHub fd0d6bffce Update FUNDING.yml (#770) 2022-07-22 12:23:27 +02:00
ArcandGitHub 14344c2f03 Update FUNDING.yml 2022-07-22 11:23:16 +01:00
calleandGitHub e4df8c52e2 add mkcert instructions (#768) 2022-07-21 14:01:16 +02:00
calleandGitHub 4feb45d745 LNURLp: NFC UI improvement (#767) 2022-07-21 13:39:21 +02:00
calleandGitHub 76a5196dbf LNURLp: Write NFC tags with LNURL-pay links (#766)
* NFC for LNURL-pay

* notification ui improvements
2022-07-21 12:38:30 +02:00
calleandGitHub 45c356cf6c change prefix to lightning: until wallets fix their bugs (#762) 2022-07-19 17:35:00 +02:00
calleandGitHub 0f73501a88 LNURLw: NFC fix uri prefix (#761)
* fix uri prefix

* prettier
2022-07-19 17:20:50 +02:00
calleandGitHub b8c99a15d1 do not mark payments as failed (#750) 2022-07-19 12:33:28 +02:00
blackcoffeexbtandGitHub fe2cbe8774 Jukebox improvements (#759)
* Improved styling of Spotify redirect URI clipboard copy text

* Jukebox extension - Moved links to Spotify into buttons to make set up steps more obvious

* Jukebox extension - Tweaks to titles of set up steps to clarify

* Prettified jukebox template
2022-07-19 12:28:49 +02:00
ed2c5cc586 Fix/lnurl auth (#757)
* remove g() and add wallet

* wallet type

* add wallet object

* send wallet as param

* make lnurlauth work

* remove g() and add wallet

* wallet type

* add wallet object

* send wallet as param

* make lnurlauth work

* black'ed

* blacked

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2022-07-19 11:35:28 +02:00
callebtc 694124b5d6 require admin 2022-07-19 11:27:32 +02:00
callebtc b2a0faf9de error check for pr 2022-07-19 11:21:54 +02:00
callebtc 8d52b1b359 check description hash 2022-07-19 11:15:11 +02:00
Lee SalminenandGitHub b6d01792e2 Add NFC Writing support to LNURLw extension (#725) 2022-07-18 16:56:15 +02:00
calleandGitHub f4580955b9 loguru warnings (#751) 2022-07-17 22:25:37 +02:00
c8ab2859fd Splitpayments: floating point percentage for split value (#690)
* allow for float in model

* recreate DB with float type

* remove rounding in UI

* black

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2022-07-17 20:00:06 +02:00
Tiago VasconcelosandGitHub 2b827d37fa return lnurl in create link (#685) 2022-07-17 14:39:02 +02:00
Lee SalminenandGitHub 8079325ec9 fix /usermanager/api/v1/extensions (#730)
Requests to `POST /usermanager/api/v1/extensions` would silently fail due to a missing `await`.
2022-07-17 14:38:00 +02:00
calleandGitHub 6646fce549 Core: add internal flag for invoice creation to use FakeWallet (#646)
* add internal flag for invoice creation to use FakeWallet
2022-07-17 14:34:25 +02:00
calleandGitHub d4c30a1091 remove old files (#748) 2022-07-17 14:04:59 +02:00
calleandGitHub 2febb364ca lnurldevice migration: typo fix (#746)
* typo fix

* fix table name

* revert typo
2022-07-17 13:22:23 +02:00
calleandGitHub ad2aad05e0 CI: Migration SQLite to PostgreSQL (#719)
* migrate all extensions
* errors if migration is missing
2022-07-17 13:11:13 +02:00
calleandGitHub f4e7d62ca3 make format everything (#743) 2022-07-16 14:23:03 +02:00
calleandGitHub 69516bb751 regtest on push (#742) 2022-07-16 14:17:45 +02:00
calleandGitHub 99b6b9d71c CI: regtest with LndRestWallet and CLightningWallet (#738)
* add regtest
* test LndRestWallet
* test CLightningWallet
2022-07-16 14:10:43 +02:00
6a3a72b944 Uvicorn loguru format (#741)
* use loguru formatting for uvicorn logs

* remove depth from logging interceptor

* format for uvicorn

* black

* isort

* black again

* python3-dev

* python3-dev

* leaner pipeline

Co-authored-by: dni <dni.khr@gmail.com>
2022-07-15 19:54:12 +02:00
Tiago VasconcelosandGitHub 348033da1b python code styling (#739) 2022-07-15 19:11:11 +02:00
ArcandGitHub 37c7553c19 Update installation.md 2022-07-15 08:18:07 +01:00
matthewcroughan 984f06239f nix: remove forAllSystems from nixosModule
This causes lnbits.nixosModules.default to exist, rather than
lnbits.nixosModules.aarch64-linux.default. The latter was a mistake.
2022-07-14 05:32:16 +01:00
calleandGitHub 1f13bdeb2f Pipenv/fix install loguru (#732)
* update pipfile

* pipenv workflow

* pipfile jinja

* Pipfile installs dev files

* python3.8 tests only

* version tag yaml

* all tests

* pipenv with pip

* only 3.7

* activate env

* test-pipenv

* pipenv importlib_metadata

* importlib-metadata

* 3.8

* 3.7 only oops

* merge install guides

* install guide in README
2022-07-13 01:15:48 +02:00
calleandGitHub 357860d849 Spark: increase timeout and improved error handling (#718)
* increase timeout and more error handling

* more error handling

* add timeout exception
2022-07-12 13:22:55 +02:00
41dda6fd01 Revert "Fixed psycopg2 error on aarch64" (#729)
* Revert "Fixed psycopg2 error on aarch64 (#658)"

This reverts commit 8a20b779df.

* remove psycopg from build pipeline

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
2022-07-11 17:18:28 +02:00
BitkarrotandGitHub 46462b4f01 Update extensions.md (#724)
suggestion for gsed on macOS, as sed may not work

reference: https://apple.stackexchange.com/questions/414568/i-get-an-error-sed-1-12d-invalid-command-code-when-i-use-sed-12d-samp
2022-07-11 13:40:46 +02:00
a57980469b addition to dockerignore (#722)
* add venv, data, tests to dockerignore

* add config files to ignore

Co-authored-by: dni <dni.khr@gmail.com>
2022-07-11 13:40:22 +02:00
matthewcroughan 6aa8330dc3 Start using Nix as a reproducible build tool 2022-07-08 11:06:26 +01:00
matthewcroughan 36e92765d3 treewide: use StaticFiles() rather than rel path
This commit is not exhaustive, and it is a trend in this codebase to not
use StaticFiles() and instead use relative paths. This means the code
cannot run anywhere other than the source code directory, as it will not
find the files it is looking for
2022-07-08 11:06:26 +01:00
matthewcroughan 49bd5ad26b lnbits/server.py: init
This adds a wrapper around uvicorn using Python. This is so the
entrypoint lnbits.server can be called on the command line, and passed
arguments like port and host. This makes lnbits feel more like a real
CLI application, and lends well to running as a service in systemd
2022-07-08 11:06:23 +01:00
matthewcroughan 50a5f47308 Add Poetry as a build system 2022-07-08 09:22:35 +01:00
matthewcroughan c4924bcf24 gitignore: add Nix ignores 2022-07-08 09:20:38 +01:00
calleandGitHub 0825519361 None check (#720) 2022-07-07 21:58:39 +02:00
b1d6c82466 fix dockerfile and add docker ignore (#714)
* fix dockerfile and add docker ignore

* fixup comment in dockerignore

Co-authored-by: dni <dni.khr@gmail.com>
2022-07-07 18:30:21 +02:00
calleandGitHub 63d4e60542 Testing: postgres db backend (#711)
* try postgres run

* fix yaml

* test with postgres

* check with postgres

* inkey_from

* remove trio

* add coverage

* add coverage

* more python testing

* use @pytest_asyncio.fixture

* remove unused imports

* fix api_payment payment lookup

* measure durations
2022-07-07 18:29:26 +02:00
calleandGitHub 262bd32f44 debug format more expressive (#715) 2022-07-07 16:24:36 +02:00
calleandGitHub 089313f613 Logging with loguru (#708)
* logging

* requirements

* add loguru dependency

* restore it

* add loguru

* set log level in .env file

* remove service fee print

* set log level

* more logging

* more logging

* more logging

* pyament.checking_id

* fix
2022-07-07 14:30:16 +02:00
847fd18796 update readme link (#687)
Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2022-07-07 00:14:27 +02:00
ArcandGitHub ed6ddd0f5e Merge pull request #700 from leesalminen/improved-pwa
Improved support for Progressive Web Apps (PWA)
2022-07-06 11:18:03 +01:00
Lee Salminen 94e4495026 run prettier 2022-07-05 16:16:46 -06:00
Lee Salminen aa51d85d65 Merge branch 'main' into improved-pwa 2022-07-05 15:10:57 -06:00
Lee Salminen ebeb3d213b alter injection of tpos to match 2022-07-05 15:09:04 -06:00
Lee Salminen 2cb87d96f2 better way of handling injection of webmanifests 2022-07-05 15:05:31 -06:00
Tiago VasconcelosandGitHub 1b064ceda4 fix issue #683 (#684) 2022-07-05 21:40:05 +02:00
calleandGitHub 92887ed169 Format: prettier (#707)
* format: prettier

* gh-actions: add prettier checkg

* actions
2022-07-05 21:14:56 +02:00
calleandGitHub d55f8dc36e readd prettier (#706) 2022-07-05 17:52:01 +02:00
Lee Salminen 80f8d83548 Merge remote-tracking branch 'origin/main' into improved-pwa 2022-07-05 09:21:41 -06:00
blackcoffeexbtandGitHub e5b4c54348 Added line wrapping for text in code examples to make it readable on normal sized screens (#688) 2022-07-05 17:20:59 +02:00
blackcoffeexbtandGitHub f44bd55b4b Corrected example CURL requests in multiple extension API docs (#689)
* Updated api docs example CURL calls for satspay server

* Updated api docs example CURL calls for copilot extension

* Updated api docs example CURL calls for jukebox extension

* Updated api docs example CURL calls for livestream extension

* Updated api docs example CURL calls for lnaddress extension

* Updated api docs example CURL calls for lnurldevices extension

* Updated api docs example CURL calls for lnurlp extension

* Updated api docs example CURL calls for offlineshop extension

* Updated api docs example CURL calls for paywall extension

* Updated api docs example CURL calls for satsdice extension

* Updated api docs example CURL calls for splitpayments extension

* Fix typo double slash in satsdice extension api docs

* Updated api docs example CURL calls for tpos extension

* Updated api docs example CURL calls for watchonly extension
2022-07-05 17:20:42 +02:00
calleandGitHub 17c79cd044 Makefile: remove prettier (#705)
* remove prettier

* black
2022-07-05 17:18:22 +02:00
Lee Salminen 56c60587a1 fix prettier 2022-07-05 09:14:24 -06:00
ArcandGitHub c2399cf9aa Merge pull request #697 from leesalminen/tpos-tips
Enhancements to TPOS Extension
2022-07-05 13:16:55 +01:00
Lee Salminen f9b929ae7d add PWA support for the tpos extension 2022-07-04 16:39:05 -06:00
Lee Salminen 52b8506d0a reduce noise 2022-07-04 16:10:31 -06:00
Lee Salminen 102dc56c4a cant force the color 2022-07-04 13:38:05 -06:00
Lee Salminen b298e12cd3 add offline indicator 2022-07-04 13:03:21 -06:00
Lee Salminen d88ffeb237 segregate cache by api key header 2022-07-04 12:32:28 -06:00
Lee Salminen d0ba5c6f30 Improved support for Progressive Web Apps (PWA)
- modify webmanifest to use LNBITS_SITE_TITLE and LNBITS_CUSTOM_LOGO env vars
- modify webmanifest to have a more accurate description, start_url and set theme_color to match default lnbits theme
- add service worker to cache requests (chrome requires a registered service worker to activate some pwa functionality)
- move webmanifest to <head> (chrome acts weird with it in the body)
2022-07-04 11:01:08 -06:00
ArcandGitHub dd9ed3bf78 Merge pull request #694 from dni/patch-1
Ignore docker directory
2022-07-04 16:20:04 +01:00
Lee Salminen 82d7bfbba8 Enhancements to TPOS Extension
- Add new tip % option to TPOS extension.
        - When adding a new TPOS, a user can choose 1 or more tip % options to be displayed to the customer.
        - When adding a new TPOS, a user can choose a wallet to send all collected tips to.

- UI Refresh on TPOS extension.
        - Moved the share button to the top navigation, next to the TPOS name, and changed the icon to a more recognizable one.
        - Re-arranged the buttons on the keypad to be more ergonomic.
2022-07-03 15:49:31 -06:00
Daniel KrahoferandGitHub 0ae1cf6e52 Ignore docker directory
Ignore docker directory if you clone and use the https://github.com/lnbits/legend-regtest-enviroment
2022-06-30 12:03:46 +02:00
ArcandGitHub e4f16f172c Merge pull request #692 from lnbits/fix/hotfix_apipayment
hotfix api_payments failing
2022-06-29 14:21:15 +01:00
Tiago vasconcelos 6154454f15 hotfix api_payments failing 2022-06-29 10:55:54 +01:00
calleandGitHub e7dd3ec64f reactivate actions (#682)
* reactivate other actions

* black

* codeql 2

* black only

* remove mock event loop

* do not follow grpc
2022-06-27 01:11:35 +02:00
calleandGitHub f6da260464 Automated tests (#566)
* return error for wrong key

* payment check use key dependency

* more expressive error

* re-add optional key

* more tests

* more

* more granular

* more testing

* custom event_loop

* tests work

* fix lots of mypy errors

* test_public_api

* both files

* remove unused import

* tests

* tests working

* rm empty file

* minimal test

* set FAKE_WALLET_SECRET="ToTheMoon1"

* set FAKE_WALLET_SECRET="ToTheMoon1"

* trial and error

* trial and error

* test postgres

* test postgres

* test postgres

* test postgres

* test postgres

* test postgres

* test build

* skip mypy
2022-06-27 00:11:46 +02:00
calleandGitHub 2f62d98299 Delete expired invoices after one pending check (#531)
* delete expired invoices regularly and after pending check

* fix mistake

* delete expired invoices only once
2022-06-26 19:12:55 +02:00
calleandGitHub 2c68f53c91 /api/v1/payments with limit and offset (#594) 2022-06-26 19:05:34 +02:00
mrbitcoinerandGitHub 8a20b779df Fixed psycopg2 error on aarch64 (#658) 2022-06-22 15:37:14 +02:00
calleandGitHub 06fbc4b0ae move fiat_base_multiplier multiplication into api_link_create_or_update() (#675) 2022-06-22 12:28:40 +02:00
ArcandGitHub 6de20106ef Merge pull request #672 from talvasconcelos/fix/logo_placement
quik fix logo placement
2022-06-21 10:50:51 +01:00
Tiago vasconcelos d8445450d8 quik fix logo placement 2022-06-21 10:45:09 +01:00
ArcandGitHub b90eb0a3ba Merge pull request #648 from lnbits/black_formatting
Code cleanup: black formatting
2022-06-20 20:57:44 +01:00
ArcandGitHub 4e25da5ca0 Merge pull request #671 from talvasconcelos/fix/error_response
Hotfix for JSON response
2022-06-20 20:54:12 +01:00
Tiago vasconcelos 3e8d2a5ba1 fix response 2022-06-15 16:49:39 +01:00
Tiago vasconcelos 1ebba9f846 send json response basic error 2022-06-15 16:37:10 +01:00
Tiago vasconcelos 626a93a46c add json response for basic error 2022-06-15 16:20:20 +01:00
ArcandGitHub 02e0efe722 Merge pull request #667 from talvasconcelos/fix/error_response
Fix HTML response to non browser request
2022-06-15 15:49:52 +01:00
calleandGitHub 1dec874af7 lnurlp: do not divide twice (#669) 2022-06-15 12:48:13 +02:00
ArcandGitHub 8e9972ead1 Merge pull request #632 from bennyhodl/patch-1
Fix GET transactions endpoint in usermanager docs
2022-06-15 09:10:47 +01:00
ArcandGitHub 399ff11b04 Merge pull request #663 from lnbits/lnurlp/float_fiat_amount
lnurlp: support for floating point fiat values
2022-06-15 09:06:37 +01:00
ArcandGitHub 785c150bb2 Merge pull request #668 from lnbits/primarycolor
Changes warning model title to primary color
2022-06-15 09:02:02 +01:00
Tiago vasconcelos c4f9206c10 change status code 2022-06-13 21:43:17 +01:00
ben 2ff282ce4c Chnaged to primary 2022-06-13 21:27:24 +01:00
Tiago vasconcelos 5ceb8639f9 clean up and comment 2022-06-13 16:18:16 +01:00
Tiago vasconcelos 9279129f07 check if request is from browser 2022-06-13 15:18:14 +01:00
callebtc c5ddd1a2c6 fiat_base_multiplier also in rate conversion 2022-06-10 16:08:29 +02:00
callebtc 6617450c21 store fiat_base_multiplier in db 2022-06-10 16:05:33 +02:00
callebtc b7008495b8 lnurlp: support for floating point fiat values 2022-06-10 15:35:35 +02:00
77fbea25af Allow for custom image logo (#642)
* first test

* add custom URL logo

* some styling

* fixed typo, wrong import

* Update .env.example

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

Co-authored-by: calle <93376500+callebtc@users.noreply.github.com>
2022-06-08 16:23:36 +02:00
calleandGitHub 906f0166bd Core: fix internal webhooks (#649)
* fix internal webhooks

* argument annotation
2022-06-03 14:33:31 +02:00
mrbitcoinerandGitHub b03da7b9c2 Added maxfee paramenter to Spark pay method (working on c-lightning 0.11.0.1) (#641) 2022-06-02 12:11:48 +02:00
HODLmeTightandGitHub 145b69b259 Adjusted sequencing of the migration script (#651)
lnbits needs to run on postgres once before `python3 conv.py` actually works
2022-06-02 08:51:02 +02:00
Tomas BezouskaandGitHub 895d9d2e0a Extension: LNURLw webhook_url (#610)
* LNURLw web_hook

* crlf -> lf

* Fix typo

* LNURLw webhook api doc
2022-06-01 15:24:17 +02:00
callebtc f8400512f7 black formating 2022-06-01 14:53:05 +02:00
Tiago VasconcelosandGitHub 7c4ce9bf96 Extension: add example extension (#644)
* add example ext files

* remove from extension list
2022-06-01 13:53:47 +02:00
calleandGitHub b4a6b5a1d9 serviced fix doc (#645) 2022-06-01 13:25:37 +02:00
ArcandGitHub 11006ef7ed Merge pull request #633 from lnbits/revert-619-check_invoice_status_outgoing
Revert "Check invoice status outgoing"
2022-05-20 14:35:46 +01:00
ArcandGitHub b4ad0e101d Revert "Check invoice status outgoing" 2022-05-20 14:35:00 +01:00
₿enandGitHub cbf067bba2 Fix GET transactions endpoint in usermanager docs 2022-05-19 23:05:34 -04:00
ArcandGitHub 2a53ee00fc Merge pull request #628 from lnbits/lnurlwuniquefix
Fixed unique LNURLw bug
2022-05-18 11:14:39 +01:00
ben c23c8d9a37 Fixed unique LNURLw bug 2022-05-18 11:08:52 +01:00
ArcandGitHub 1fa9ac2622 Merge pull request #627 from lnbits/lnurlwfisx
Quick lnurlw fix
2022-05-17 22:09:52 +01:00
ben d951ac435f Added check to form 2022-05-17 22:08:33 +01:00
ben ca8f649fd6 Added check for error message 2022-05-17 22:02:54 +01:00
ben bb5b04b307 Added max amount to withdraw creation 2022-05-17 21:55:39 +01:00
ArcandGitHub 313c40dc2b Merge pull request #609 from ashkanjalaliQ/patch-1
Update _api_docs.html
2022-05-17 14:20:20 +01:00
ArcandGitHub 9d6382294a Merge pull request #616 from talvasconcelos/eclair-backend
Add Eclair backend/funding wallet
2022-05-17 14:19:28 +01:00
ArcandGitHub 2337471e45 Merge pull request #619 from lnbits/check_invoice_status_outgoing
Check invoice status outgoing
2022-05-17 14:17:41 +01:00
ArcandGitHub aeb8c27ab2 Merge pull request #620 from talvasconcelos/fix/usermanager_permissions
protect listing and deleteing user and wallets
2022-05-17 14:16:48 +01:00
ArcandGitHub 46feaab142 Merge pull request #624 from talvasconcelos/fix/bookmarkPrompt
get bookmark promp fired on new wallet
2022-05-17 14:15:43 +01:00
Tiago vasconcelos 98891b7680 get bookmark promp fired on new wallet 2022-05-17 12:16:41 +01:00
ArcandGitHub 4391cd6fc0 Merge pull request #623 from talvasconcelos/fix/renameWallet
key issues fixed
2022-05-16 11:27:06 +01:00
Tiago vasconcelos c8aaf3377f clean logs 2022-05-16 11:23:34 +01:00
Tiago vasconcelos caa23e6c9d key issues fixed 2022-05-16 11:21:30 +01:00
Tiago vasconcelos a0a2ecd424 protect listing and deleteing user and wallets 2022-05-11 16:30:15 +01:00
ArcandGitHub 2d5f30acfd Merge pull request #618 from joelklabo/user-manager-doc-typo
Fix typo in User Manager API Docs
2022-05-11 05:28:57 +01:00
callebtc ebd6d6d58b key check 2022-05-10 22:00:50 +02:00
callebtc f08fbc2487 core: check_invoice_status for outgoing payments 2022-05-10 21:48:24 +02:00
Joel KlaboandGitHub c04e1de412 Fix typo in User Manager API Docs 2022-05-07 17:27:58 -07:00
fiatjaf 436c3dd51a fix: spark fee when pay_invoice pending. 2022-05-06 20:10:42 -03:00
Bitcoin Gamer 21andGitHub 0c089109b9 Edited discord bot extension to use FastAPI (#570)
Squashed 9 commits:
* Added Discordbot Extension
* Removed backup files
* Fixing renaming wallet
* added further description
* further description index.html
* import staticfiles
* remove duplicate file
* deleted changes in core
2022-05-06 20:26:42 +02:00
Tiago vasconcelos e0504a4e0c add eclair variables to .env example 2022-05-05 15:55:01 +01:00
Tiago vasconcelos 1695d0ce0e send 0 fees when not available 2022-05-05 15:47:25 +01:00
Tiago vasconcelos 96df280d49 adding fees 2022-05-05 15:44:34 +01:00
Tiago vasconcelos 2cd6626ca1 get status 2022-04-29 11:39:27 +01:00
Tiago vasconcelos 96c7decae7 Merge branch 'main' into eclair-backend 2022-04-28 15:30:26 +01:00
Ashkan JalaliandGitHub d77e304e12 Update _api_docs.html
add "webhook" and "unit" arguments to api document
2022-04-28 17:07:58 +04:30
ArcandGitHub 8dbe1b3678 Merge pull request #603 from lnbits/offTheme
Adds Freedom theme to celebrate Oslo Freedom Forum
2022-04-20 11:25:35 +01:00
benarc 2fd12e1d19 Changed theme picker color 2022-04-20 11:23:11 +01:00
benarc a36ad05fe0 Added .env for theme 2022-04-20 11:20:39 +01:00
benarc e6432f0e29 Merge remote-tracking branch 'origin/main' into offTheme 2022-04-20 11:17:17 +01:00
ArcandGitHub c539bcbbb1 Merge pull request #602 from lnbits/tposprettier
prettier tpos
2022-04-20 11:16:49 +01:00
benarc 8042dc51b3 prettier tpos 2022-04-20 11:16:00 +01:00
ArcandGitHub ed48821ef9 Merge pull request #601 from lnbits/tposbuttons
TPoS now theme dependent
2022-04-20 10:58:41 +01:00
benarc 1874d7fd54 TPoS now theme dependent 2022-04-20 10:42:55 +01:00
benarc 165d663491 Added freedom forum theme 2022-04-20 00:36:11 +01:00
ArcandGitHub d1d252210c Merge pull request #595 from lnbits/lnurlwlist
Adds csv list to lnurlw extension
2022-04-16 10:52:56 +01:00
benarc 9818ba4677 removed qrcode library 2022-04-16 10:50:12 +01:00
benarc 6fdd1fb24b updated link 2022-04-16 10:47:58 +01:00
benarc 953e07eeef Added csv export to lnurlw 2022-04-16 10:37:36 +01:00
ArcandGitHub c229fb9ab3 Merge pull request #588 from talvasconcelos/fix/renameWallet
Fix the renaming of wallets
2022-04-15 22:27:35 +01:00
ArcandGitHub df9a088717 Merge pull request #591 from iBobik/patch-1
Fixed docs about LND_REST_ENDPOINT
2022-04-15 22:26:12 +01:00
Jan PobořilandGitHub 66529c10d0 Fixed docs about LND_REST_ENDPOINT
There should be base URL, not only hostname.
2022-04-13 17:20:24 +02:00
Tiago vasconcelos 13f490b586 Merge branch 'main' into fix/renameWallet 2022-04-12 10:19:31 +01:00
fiatjaf 2f381960a0 fix sparko invoice checking ("expired" was being considered as "paid"). 2022-04-05 08:44:41 -03:00
arcbtc 810b918619 quick fix 2022-04-02 00:36:50 +01:00
ArcandGitHub 81860bdc5d Merge pull request #579 from ashkanjalaliQ/main
add "IRT" currency to TPos extention
2022-04-01 23:57:19 +01:00
Ashkan JalaliandGitHub c5c76468cb add "IRT" currency to TPos extention 2022-04-01 20:05:53 +04:30
Tiago vasconcelos 950408ef64 Merge branch 'main' into fix/renameWallet 2022-04-01 10:42:16 +01:00
fusion44andGitHub 799065e982 Merge pull request #576 from lilcheti/IRT-api
add IRT api
2022-03-31 18:52:53 +02:00
rimthekid a94444ae2c add IRT 2022-03-30 05:22:48 -07:00
ArcandGitHub 54264315f0 Merge pull request #575 from axelhodler/exception_handling
fail early if LNBITS_DATA_FOLDER missing
2022-03-30 12:04:28 +01:00
Axel Hodler 946ebc350e fail early if LNBITS_DATA_FOLDER missing 2022-03-27 17:03:24 +02:00
calleandGitHub b1c75813a0 fix decodepay (#568) 2022-03-24 12:50:57 +01:00
ArcandGitHub a17108f108 Update installation.md 2022-03-22 19:25:30 +00:00
Tiago vasconcelos c59d0a51ff change key on PUT request 2022-03-18 20:58:47 +00:00
calleandGitHub 1209526147 remove get_payment_status (#562)
* remove get_payment_status

* remove comments
2022-03-18 18:51:28 +01:00
calleandGitHub 0f97f8f18b Fee reserve for lightning backends (#557)
* preparing fees

* fee_limit_msat

* await resp result

* clightning

* fix tests

* fix test

* add fee to test

* mypy

* invoice_status

* checking id fix

* fee reserve error message

* only for external payments
2022-03-16 07:20:15 +01:00
ArcandGitHub 911fe92e03 Merge pull request #559 from lnbits/withdrawpayzerocheck
Added check so number must be more than zero
2022-03-14 11:30:08 +00:00
Ben Arc eb8bbd3896 Added check so number must be more than zero 2022-03-14 11:22:56 +00:00
Ben Arc ff70efd1eb Removes LNDRPC, if someone wants to fix we can implement again 2022-03-11 14:13:28 +00:00
Ben Arc ce6620882f Satspay tooltip fix (quart commit 8e3230287dd191a743479ee9ceb2f497c43dd9bc) 2022-03-11 13:25:14 +00:00
Ben Arc ffba97850b Stopped 0 invoice creation in tickets 2022-03-11 13:12:27 +00:00
Ben Arc 435b4056fd fixed conversion api 2022-03-10 12:16:51 +00:00
Ben Arc 30a0d6336b removed print 2022-03-08 22:29:13 +00:00
Ben Arc 52ef2da76d added wrong key type 2022-03-08 22:22:51 +00:00
Ben Arc e4dd0972a5 Wrong key int being returned 2022-03-08 22:15:45 +00:00
Ben Arc c708cb1e2b Important fix to payment check 2022-03-07 18:27:03 +00:00
ArcandGitHub 43f9394014 Merge pull request #555 from lnbits/extraPayDetails
Adds more payment details if the correct key is providedd
2022-03-07 08:20:07 +00:00
benarc 9bbf78e026 Assigned wallet var 2022-03-07 08:19:14 +00:00
ArcandGitHub 6df951b873 Merge pull request #554 from lnbits/extraPayDetails
Adds more payment details if the correct key is provided
2022-03-07 08:11:11 +00:00
benarc d62ef0bb80 Added extra pay details 2022-03-07 08:08:08 +00:00
benarc 4a9e7f4264 Merge remote-tracking branch 'origin/main' into main 2022-03-07 05:56:10 +00:00
ArcandGitHub b886e5b0ea Merge pull request #528 from CandleHater/patch-2
added "missing" libffi/libpq package
2022-03-07 05:47:29 +00:00
benarc 72f2ec0be8 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:26:40 +00:00
ArcandGitHub ef6d12fe10 Merge pull request #545 from talvasconcelos/fix/paymentsDecode
fix payments decode
2022-03-07 04:24:32 +00:00
ArcandGitHub 1e759283ce Merge pull request #540 from talvasconcelos/fix/watchonly_mempool
fix update mempool
2022-03-07 04:19:23 +00:00
ArcandGitHub e92447092d Merge pull request #532 from lnbits/retry_connection_on_fail
Retry backend connection on startup
2022-03-07 04:18:57 +00:00
ArcandGitHub 7988e2c303 Merge pull request #541 from talvasconcelos/fix/streamlabsInv
fix streamalerts
2022-03-07 04:17:15 +00:00
ArcandGitHub a13bb9cfea Merge pull request #542 from talvasconcelos/fix/copilot_db
fix copilots not being fetched
2022-03-07 04:16:41 +00:00
ArcandGitHub 7088a3240f Merge pull request #543 from talvasconcelos/fix/tipjar
change namedtuple to basemodel
2022-03-07 04:16:16 +00:00
ArcandGitHub 00e52001b5 Merge pull request #544 from talvasconcelos/fix/events
fix typo in table name
2022-03-07 04:15:54 +00:00
ArcandGitHub cd35966a56 Merge pull request #546 from talvasconcelos/fix/jukebox
fix jukebox
2022-03-07 04:15:28 +00:00
ArcandGitHub c59a65cdb4 Merge pull request #550 from talvasconcelos/fix/lnticket
Tickets issue #548
2022-03-07 04:14:16 +00:00
benarc 5f52658626 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:05:43 +00:00
Tiago vasconcelos 2399b05855 open ticket in dialog for large text 2022-03-03 23:05:34 +00:00
Tiago vasconcelos 8fb70041d2 fix flatrate per word toggle 2022-03-03 21:35:58 +00:00
Joseph PentlandandGitHub baf55afc28 api: Fix conversion from fiat to satoshis (#516)
Conversion to satoshis incorrectly used data.to as fiat currency rather
than data._from.
2022-03-01 23:11:39 +03:00
Graham KrizekandGitHub 251e6a8233 Add a fallback to LND_REST_CERT to use the system chain if its not present (#539) 2022-03-01 22:58:54 +03:00
ArcandGitHub d73c3950b9 Update README.md 2022-03-01 19:07:21 +00:00
ArcandGitHub ac61cd5427 Update README.md 2022-03-01 19:07:07 +00:00
ArcandGitHub 0c14b8fb82 Merge pull request #533 from louneskmt/fix-docker-image-build
Fix Docker image autobuild GitHub Action and build for arm
2022-03-01 18:57:45 +00:00
Tiago vasconcelos 6c0aa8fa8d info/error if no music is playing yet on jukebox 2022-02-28 10:09:40 +00:00
Tiago vasconcelos 9b95a7743d fix user keyword to "user" 2022-02-28 10:08:50 +00:00
benarc e96701cb59 Added template banner 2022-02-24 16:03:10 +00:00
benarc c1fa5bbcbf Change HIDE to false 2022-02-24 12:32:37 +00:00
Tiago vasconcelos 241c81c790 fix update mempool 2022-02-23 22:09:38 +00:00
Tiago vasconcelos 42b444f54d make "anonymous" as default name 2022-02-23 21:40:11 +00:00
Tiago vasconcelos f7ff9d27d2 fix copilots not being fetched 2022-02-23 21:03:25 +00:00
benarc 4a588dda1b Added ad space and option to turn off api docs 2022-02-23 13:51:30 +00:00
Tiago vasconcelos e82ce89e9a change namedtuple to basemodel 2022-02-23 13:32:23 +00:00
Tiago vasconcelos 270cac37ac fix typo in table name 2022-02-23 10:37:15 +00:00
Tiago vasconcelos a374526faf make Anonymous default name 2022-02-23 10:13:52 +00:00
Tiago vasconcelos 8930152b0f fix models int to str 2022-02-22 21:22:07 +00:00
Tiago vasconcelos 71e11b7387 fix typo 2022-02-22 21:12:46 +00:00
Tiago vasconcelos 510a28f3d0 create_charge data 2022-02-22 20:38:35 +00:00
ArcandGitHub c58e2c5d74 Merge pull request #538 from talvasconcelos/fix/withdrawQR
Fix LNURLw QR code
2022-02-22 18:28:29 +00:00
Tiago vasconcelos eb7b369b6d fix payments decode 2022-02-22 16:45:35 +00:00
Tiago vasconcelos a5a3331106 revert to fallback scheme + make request case insensitive 2022-02-22 16:23:50 +00:00
Tiago vasconcelos 4f3e48e280 fix lnurl encoded in QR 2022-02-22 15:16:39 +00:00
Lounès Ksouri 31bcb32e2d feat: dynamic port and host using environment variables 2022-02-22 08:41:57 +01:00
Lounès Ksouri a132387fa8 fix: add build deps for arm 2022-02-22 08:41:19 +01:00
Lounès Ksouri 27694ee23d refactor: GitHub Action to build and push to Docker Hub on tag 2022-02-22 08:40:23 +01:00
callebtc 36952981c2 remove success var 2022-02-21 09:25:45 +01:00
callebtc fbafe2b8f1 Retry backend connection on startup 2022-02-21 09:17:36 +01:00
ArcandGitHub 41a9689014 Update installation.md 2022-02-18 17:17:18 +00:00
ArcandGitHub 76edfa2134 Update installation.md 2022-02-18 17:16:49 +00:00
ArcandGitHub d7b3a28950 Update installation.md 2022-02-18 17:16:18 +00:00
ArcandGitHub 2c161a079d Update installation.md 2022-02-18 17:16:03 +00:00
CandleandGitHub 4793ea0d18 added "missing" libffi/libpq package
When I tryed to run "pipenv install --dev" on a Pi 4 with the latest Raspberry OS it failed with two dependencies. These additional installations fixed it.
2022-02-18 16:35:32 +01:00
ArcandGitHub 88df4266b7 Merge pull request #524 from lnbits/docs_api_add_unit_to_invoice
Fix fiat amount entry in invoice creation
2022-02-18 11:44:05 +00:00
callebtc 6496fac35f fill mask for fiat 2022-02-18 12:35:25 +01:00
Ben Arc 10492ec3d0 Changed bitcoin theme back 2022-02-17 23:40:29 +00:00
Ben Arc 302d79a30f Improved bitcoin theme 2022-02-17 23:33:55 +00:00
ArcandGitHub b88abd1a46 Merge pull request #527 from talvasconcelos/fix/instantWallet
Fix instant wallet form LNURLw
2022-02-17 23:21:58 +00:00
Ben Arc 573c4cf5ea Changed theme order to default to classic 2022-02-17 21:46:51 +00:00
Ben Arc e9065833c0 Fixed themes, added bitcoin theme 2022-02-17 21:34:36 +00:00
Tiago vasconcelos 251ecb8f1d url_for params 2022-02-17 21:01:14 +00:00
calleandGitHub c626e12fa3 LNDHhub for fastapi with LIMIT (#499)
* add limit to get_payments

* add limit to lndhub

* fix defaults

* default memo

* add offset cluase

* offset for the api
2022-02-17 13:10:34 +01:00
callebtc 13a72d9a41 replace string with LNBITS_SITE_TITLE 2022-02-17 11:42:08 +01:00
benarc 3c5fac3874 Fixed paywall, unnecessary key check added 2022-02-17 09:23:33 +00:00
callebtc 15c8557459 allow empty memo 2022-02-17 10:16:11 +01:00
callebtc d559c604fa invoice without memo 2022-02-17 09:29:10 +01:00
callebtc cd84623305 amount label adaptive to uni - look at me Im a frontent dev now 2022-02-17 09:14:07 +01:00
callebtc 9fb30080ab black 2022-02-16 22:42:27 +01:00
callebtc 30ab519c3a amount is float and sats are int 2022-02-16 22:41:12 +01:00
callebtc 914b9f3ffe allow amounts >0 2022-02-16 22:23:26 +01:00
0764f4fdf1 Support for encrypted macaroons (#521)
* encrypted macaroons

* fix GRPC env entry

* example config entry

* add pycryptodomex to requirements

* documentation

* Added pycryptodomex to pip file

Co-authored-by: Ben Arc <ben@arc.wales>
2022-02-14 17:54:05 +01:00
Ben Arc 0367ee85a7 removed dense from lower buttons 2022-02-14 15:51:45 +00:00
Ben Arc 0aef9d4d9a Made dense to save space 2022-02-14 15:48:56 +00:00
Ben Arc 8967011cbe Merge branch 'mobileui' into main 2022-02-14 15:38:17 +00:00
Ben Arc 682226c4b6 chnaged button names 2022-02-14 15:36:10 +00:00
Ben Arc c7e501ac08 Adds lower buttons to wallet UI for smaller screens 2022-02-14 15:29:10 +00:00
calleandGitHub 8d7302e4cd fix bleskomat test (#523) 2022-02-14 13:49:15 +01:00
callebtc 09b86605dd fix bleskomat tests 2022-02-14 13:42:43 +01:00
orangesurfandGitHub 9e8376b3fb Remove suggestion to switch to FastAPI branch (#520)
Branch does not exist
2022-02-14 13:38:22 +01:00
ArcandGitHub 1a492cbfb8 Update installation.md 2022-02-14 11:42:19 +00:00
ArcandGitHub 155c32504a Update api.py 2022-02-14 11:31:42 +00:00
ArcandGitHub 8c923ad653 Update README.md 2022-02-14 09:23:04 +00:00
ArcandGitHub 25b9014c3d Update README.md 2022-02-14 09:22:23 +00:00
ArcandGitHub 9807e7d646 Merge pull request #518 from talvasconcelos/fix/disapearingAdminExt
Disapearing Extensions in Admin Only
2022-02-11 11:17:55 +00:00
ArcandGitHub 728ab517f5 Merge branch 'main' into fix/disapearingAdminExt 2022-02-11 11:17:37 +00:00
benarc f1fb569e2b lnurlpayout fix 2022-02-11 10:03:26 +00:00
Tiago vasconcelos 6d58027147 remove print 2022-02-10 17:06:15 +00:00
Tiago vasconcelos 4a35f84e95 fix admin default false 2022-02-10 16:44:47 +00:00
ArcandGitHub f7b37fb843 Merge pull request #498 from pseudozach/main
fix livestream startup issue
2022-02-10 15:24:05 +00:00
ArcandGitHub 699bdd9a06 Merge pull request #497 from bitkarrot/main
fix curl example paths in API docs on LNURLw extension
2022-02-10 15:21:28 +00:00
benarc b5aad94ecd rolled back admin fix 2022-02-10 13:55:45 +00:00
benarc 0f79213b0f Tals admin extension fix 2022-02-10 10:09:44 +00:00
ArcandGitHub c1654a21d4 Merge pull request #515 from lnbits/revert-513-fix/disapearingAdminExt
Revert "fix disapearing admin extension"
2022-02-10 09:56:15 +00:00
ArcandGitHub da6a2e772b Revert "fix disapearing admin extension" 2022-02-10 09:56:02 +00:00
ArcandGitHub 1626cd2b4b Merge pull request #513 from talvasconcelos/fix/disapearingAdminExt
fix disapearing admin extension
2022-02-10 09:55:14 +00:00
Tiago vasconcelos d1d62583c4 fix for whitespace in admin users list 2022-02-09 11:16:13 +00:00
Tiago vasconcelos 4c18eb0b34 fix disapearing admin extension 2022-02-09 10:57:24 +00:00
ArcandGitHub 35bb26b6fd Merge pull request #512 from lnbits/adminextensions
Adds admin extensions
2022-02-07 21:32:17 +00:00
benarc 2c853e7a52 slight bug 2022-02-07 21:28:48 +00:00
benarc 6bc1474c19 Organised .env 2022-02-07 20:48:48 +00:00
benarc 4b11342f0c works 2022-02-07 20:43:47 +00:00
benarc a7c65d5d60 Fixed admin debit input 2022-02-03 13:15:43 +00:00
benarc 50fcb15725 typo 2022-02-03 13:00:56 +00:00
ArcandGitHub 18ae455824 Merge pull request #507 from lnbits/fakewallet-demo
Makes sats denomination customisable for FakeWallet
2022-02-03 12:58:16 +00:00
benarc 8db19f33e3 Removed key for password that generates a key 2022-02-03 12:52:51 +00:00
benarc 8a43611232 added .env for custom denomination 2022-02-02 17:29:57 +00:00
benarc 75e78d2ab6 FakeWallet own denomination now working 2022-02-02 17:28:00 +00:00
benarc 568a1be91f Adding denomination for alternative currencies 2022-02-02 13:07:12 +00:00
Ben Arc b10a23dcc4 FakeWallet invoices to correct amount 2022-02-01 23:16:38 +00:00
Ben Arc dab943baad fixed internal lnurl 2022-02-01 23:11:26 +00:00
Ben Arc db734d4acd added hex 2022-02-01 23:04:40 +00:00
Ben Arc 0415f3d7cc Removed hex 2022-02-01 21:32:54 +00:00
Ben Arc 3927545e32 fixed buggy lnurl checks 2022-02-01 21:22:20 +00:00
Ben Arc 3f0a855526 Black 2022-02-01 20:51:40 +00:00
Ben Arc 615abffeb4 Replaced rustys hashed for hash 2022-02-01 20:22:02 +00:00
Ben Arc 31e300e05b hex description_hash in fakewallet 2022-02-01 20:17:42 +00:00
Ben Arc d2879aa80c added value to avoid error 2022-02-01 20:05:58 +00:00
Ben Arc d1b390b929 Use scope instead of prop 2022-02-01 20:02:21 +00:00
ArcandGitHub d1a78ab81b Merge pull request #504 from aljazceru/main
adding secp256k1 to requirements.txt
2022-02-01 09:08:20 +00:00
Aljaz CeruandGitHub 34b3188f96 Update requirements.txt 2022-02-01 00:51:42 +01:00
Aljaz CeruandGitHub bdf5dd55f6 adding secp256k1 to requirements 2022-02-01 00:49:54 +01:00
Ben Arc 3ae6ef25a1 trypo 2022-01-31 16:52:35 +00:00
Ben Arc 382622d5ac Fixed balance update not showing properly 2022-01-31 16:47:58 +00:00
Ben Arc 0680669e59 Fixed fake invoices registering as paid 2022-01-31 16:35:46 +00:00
Ben Arc dbab181759 Admin users can credit accounts 2022-01-31 16:29:42 +00:00
Ben Arc f5069f9699 Added admin users to .env 2022-01-31 12:04:12 +00:00
ArcandGitHub e97e20889e Merge pull request #503 from lnbits/fake-wallet
Adds FakeWallet, for a purely internal LNbits experience, able to make internal payments without a node
2022-01-31 12:07:24 +00:00
Ben Arc 68ba219096 added FakeWallet to .env 2022-01-31 12:00:55 +00:00
Ben Arc 649776951c Works 2022-01-31 11:58:39 +00:00
Ben Arc 7c3b3ac200 Fake wallet working 2022-01-31 11:07:29 +00:00
Ben Arc a147a07bab works 2022-01-31 00:12:59 +00:00
benarc 228717195b back 2022-01-30 19:43:30 +00:00
Marnixandfiatjaf f0d9870143 Update LICENSE
Update LICENSE to 2022
2022-01-30 08:27:05 -03:00
benarc d6f145eee9 Merge branch 'main' into fake-wallet 2022-01-30 10:48:20 +00:00
benarc 47300e477e Added try to lnurlpos to lnurldevice transfer to avoid crashing if lnurlpos does not exist 2022-01-30 10:44:20 +00:00
benarc c96e7068e5 initi 2022-01-30 10:41:20 +00:00
calleandGitHub b4d00a490b remove restriction for positive amount to check outgoing payments (#500) 2022-01-27 22:58:35 +01:00
callebtc 9505958306 systemd guide 2022-01-27 21:07:47 +01:00
pseudozach b00a39a7ce fix livestream startup issue 2022-01-24 15:58:51 -08:00
BitkarrotandGitHub b4dc07170f fix curl example paths 2022-01-24 14:51:39 -08:00
benarc 57eb27fcee Swaps lnurlpos for lnurldevice 2022-01-21 21:23:32 +00:00
ArcandGitHub 9e13b8eb70 Update installation.md 2022-01-18 22:29:15 +00:00
Tiago vasconcelosandfiatjaf 09bfa35cd5 avoid voidwallet crash 2022-01-17 15:52:19 -03:00
Tiago VasconcelosandGitHub 918e9be7ed Upgrade conversion endpoint (#491) 2022-01-14 09:19:30 -03:00
ArcandGitHub fc021b3736 Update installation.md 2022-01-14 10:23:22 +00:00
ArcandGitHub 6718ed678d Update installation.md 2022-01-14 10:23:02 +00:00
ArcandGitHub ab62014bf0 Update installation.md 2022-01-14 10:16:50 +00:00
ArcandGitHub 1940b8762b Update installation.md 2022-01-14 10:15:31 +00:00
ArcandGitHub 46838ca66f Update installation.md 2022-01-14 10:13:02 +00:00
ArcandGitHub 5fe9df48dd Update installation.md 2022-01-14 10:11:42 +00:00
ArcandGitHub 5bacb5f38a Update installation.md 2022-01-14 10:11:26 +00:00
ArcandGitHub 0f70b346ca Update installation.md 2022-01-14 10:10:52 +00:00
ArcandGitHub f162d14e41 Update .env.example 2022-01-14 09:43:30 +00:00
ArcandGitHub c87aa3dd2b Update installation.md 2022-01-14 09:38:04 +00:00
ArcandGitHub 7812c4aa48 Update installation.md 2022-01-14 09:37:27 +00:00
ArcandGitHub fefa8ba56e Update installation.md 2022-01-14 09:36:18 +00:00
ArcandGitHub 1dd8b01f5a Update installation.md 2022-01-14 09:34:46 +00:00
ArcandGitHub 81dc9303f6 Update installation.md 2022-01-14 09:34:00 +00:00
ArcandGitHub 10ccee3480 Update README.md 2022-01-14 09:28:39 +00:00
ArcandGitHub 8ca8d173e8 Update installation.md 2022-01-14 09:27:06 +00:00
ArcandGitHub 46e682bca3 Update installation.md 2022-01-14 09:20:20 +00:00
ArcandGitHub 11bf2029be Update installation.md 2022-01-14 09:19:49 +00:00
ArcandGitHub a44aba6fcf Update installation.md 2022-01-14 09:18:40 +00:00
ArcandGitHub d1a7d684b5 Update installation.md 2022-01-14 09:17:07 +00:00
benarc 4e0e5b9956 Added conversion endpoint
Exposes fiat_amount_as_satoshis
2022-01-11 15:11:48 +00:00
Tiago vasconcelosandfiatjaf 25b502e5d5 fix copilot not getting created 2022-01-10 20:52:51 -03:00
ArcandGitHub 6af8899cd1 Merge pull request #486 from cryptoteun/main
Added warning for LNULRp issues and CLightningWallet
2022-01-10 12:50:50 +00:00
cryptoteunandGitHub 7407be944c Added warning for LNULRp issues 2022-01-06 13:14:19 +01:00
Tiago vasconcelosandfiatjaf a00e46f629 Change _asdict() to dict() 2022-01-05 07:36:10 -03:00
Daniel KrahoferandGitHub aba206c87e Extension Subdomains: Update crud.py, critical sql error (#481) 2022-01-04 09:56:14 -03:00
ArcandGitHub db666fd103 Merge pull request #475 from talvasconcelos/fix/chargepermission
fix wallet key permission for create
2021-12-31 15:43:10 +00:00
ArcandGitHub 57a2dffe52 Merge pull request #476 from mrbitcoiner/main
2% reserve only for external payments
2021-12-30 17:11:37 +00:00
ArcandGitHub d5617b9462 Merge pull request #480 from talvasconcelos/issue/452
fix issue #452
2021-12-30 17:10:10 +00:00
ArcandGitHub 79ab444f59 Merge pull request #479 from talvasconcelos/issue/472
fix issue #472
2021-12-30 17:09:39 +00:00
ArcandGitHub 480546ded7 Merge pull request #478 from talvasconcelos/fix/CORS_origins
allow all origins to request from lnbits
2021-12-30 17:09:24 +00:00
ArcandGitHub 6645b156e7 Merge pull request #477 from talvasconcelos/fix/watchonly
get only one address instead of multiple
2021-12-30 17:08:23 +00:00
Tiago vasconcelos 17b13bb652 fix issue #452 2021-12-30 16:39:24 +00:00
Tiago vasconcelos 6ba88d262b fix issue #472 2021-12-30 16:21:44 +00:00
Tiago vasconcelos f76ef6b975 allow all origins to request from lnbits 2021-12-30 16:09:12 +00:00
Tiago vasconcelos 12bd930334 fix return for compatibility 2021-12-30 12:13:32 +00:00
Tiago vasconcelos b9104bf63b get only one address instead of multiple 2021-12-30 11:56:19 +00:00
Tiago vasconcelos f798ab9f57 Merge remote-tracking branch 'origin/fix/chargepermission' into fix/chargepermission 2021-12-29 15:19:24 +00:00
Tiago vasconcelos e3c6d7aebe fix :// in bitcoin 2021-12-29 15:19:09 +00:00
Tiago vasconcelos 87105b22bb fix lightning://
fix lightning://
2021-12-29 15:15:49 +00:00
Tiago vasconcelos 1fd2ccc54e fix lightning:// 2021-12-29 15:12:37 +00:00
Tiago vasconcelos aa8557ada1 fix lightning:// 2021-12-29 15:12:10 +00:00
mrbitcoiner cdeccec01b 2% reserve only for external payments 2021-12-29 11:55:52 -03:00
Tiago vasconcelos 2991cbce1f add listener to invoice 2021-12-29 12:57:43 +00:00
Tiago vasconcelos 10255ed1ce Merge remote-tracking branch 'origin/fix/chargepermission' into fix/chargepermission 2021-12-28 15:29:48 +00:00
Tiago vasconcelos df2345bda9 added require_invoice_key
clean up
2021-12-28 15:22:45 +00:00
Tiago vasconcelos adcfc43f56 clean up 2021-12-28 15:22:08 +00:00
Tiago vasconcelos aa58775c90 added require_invoice_key 2021-12-28 15:17:08 +00:00
Tiago vasconcelos 0ef0188462 index 2021-12-28 15:16:45 +00:00
Tiago vasconcelos e42fdde2ac fix wallet key permission for create 2021-12-28 14:47:44 +00:00
calleandGitHub 32ea1106f9 Crud webhook amount (#474)
* get_standalone_payment selects only incoming tx
2021-12-28 14:05:25 +00:00
fiatjaf 93e58f4c80 lnurlpos: fix query param variable is called "p", not "payload". 2021-12-22 21:55:33 -03:00
fiatjaf 6b5aaa442d lnurlpos: return lnurl error on xor_decrypt failure. 2021-12-22 21:03:56 -03:00
benarc 40aadbfec7 amount to amount_in_cent 2021-12-22 19:47:29 +00:00
ArcandGitHub a1ae7aca9e Update installation.md 2021-12-21 20:46:34 +00:00
Tiago vasconcelos 756b121105 print errors for tracking/debugging 2021-06-25 10:15:50 +01:00
Tiago vasconcelos 4585d97324 small fix on paid_invoices_stream 2021-06-23 18:50:01 +01:00
Tiago vasconcelos 021cf64c1e Added eclair backend 2021-06-23 16:13:48 +01:00
274 changed files with 9941 additions and 2880 deletions
+21
View File
@@ -0,0 +1,21 @@
.git
data
docker
docs
tests
venv
tools
*.md
*.log
.env
.gitignore
.prettierrc
LICENSE
Makefile
mypy.ini
package-lock.json
package.json
pytest.ini
-16
View File
@@ -1,16 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 2
indent_style = space
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
indent_style = space
+26 -17
View File
@@ -1,14 +1,20 @@
QUART_APP=lnbits.app:create_app()
QUART_ENV=development
QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
DEBUG=false
LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_AD_SPACE="" # csv ad image filepaths or urls, extensions can choose to honor
LNBITS_HIDE_API=false # Hides wallet api, extensions can choose to honor
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
@@ -17,8 +23,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
# disable selected extensions, or use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"
@@ -26,11 +30,12 @@ LNBITS_SERVICE_FEE="0.0"
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 mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador"
# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador"
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet
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.
@@ -44,19 +49,15 @@ SPARK_TOKEN=myaccesstoken
CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
# LnbitsWallet
LNBITS_ENDPOINT=http://127.0.0.1:5000
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY
# LndWallet
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LNPayWallet
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
@@ -72,3 +73,11 @@ LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet
OPENNODE_API_ENDPOINT=https://api.opennode.com/
OPENNODE_KEY=OPENNODE_ADMIN_KEY
# FakeWallet
FAKE_WALLET_SECRET="ToTheMoon1"
LNBITS_DENOMINATION=sats
# EclairWallet
ECLAIR_URL=http://127.0.0.1:8283
ECLAIR_PASS=eclairpw
+1 -1
View File
@@ -1 +1 @@
custom: https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
custom: https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK
+5 -5
View File
@@ -2,9 +2,9 @@ name: codeql
on:
push:
branches: [master, ]
branches: [main, ]
pull_request:
branches: [master]
branches: [main]
schedule:
- cron: '0 12 * * 5'
@@ -19,10 +19,10 @@ jobs:
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: javascript, python
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
+18 -5
View File
@@ -2,9 +2,9 @@ name: formatting
on:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
black:
@@ -15,9 +15,22 @@ jobs:
- run: python3 -m venv venv
- run: ./venv/bin/pip install black
- run: make checkblack
isort:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: sudo apt-get install python3-venv
- run: python3 -m venv venv
- run: ./venv/bin/pip install isort
- run: make checkisort
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install
- run: make checkprettier
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: sudo apt-get install python3-venv
- run: python3 -m venv venv
- run: npm install prettier
- run: ./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
+51
View File
@@ -0,0 +1,51 @@
name: migrations
on: [pull_request]
jobs:
sqlite-to-postgres:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
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.8]
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
sudo apt install unzip
- name: Run migrations
run: |
rm -rf ./data
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
unzip tests/data/mock_data.zip -d ./data
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
./venv/bin/python tools/conv.py
+2
View File
@@ -5,8 +5,10 @@ on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
if: ${{ 'false' == 'true' }} # skip mypy for now
steps:
- uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master
with:
mypy_flags: '--install-types --non-interactive'
path: lnbits
-58
View File
@@ -1,58 +0,0 @@
name: Docker build on push
env:
DOCKER_CLI_EXPERIMENTAL: enabled
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-20.04
name: Build and push lnbits image
steps:
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Show available Docker buildx platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx against commit hash
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:${GITHUB_SHA:0:7} \
--output "type=registry" ./
- name: Run Docker buildx against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:latest \
--output "type=registry" ./
+68
View File
@@ -0,0 +1,68 @@
name: Build and push Docker image on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-*"
jobs:
build:
runs-on: ubuntu-20.04
name: Build and push lnbits image
steps:
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
- name: Import environment variables
id: import-env
shell: bash
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Show available Docker buildx platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \
--output "type=registry" ./
- name: Run Docker buildx against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \
--output "type=registry" ./
+85
View File
@@ -0,0 +1,85 @@
name: regtest
on: [push, pull_request]
jobs:
LndRestWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup Regtest
run: |
docker build -t 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
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 pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: LndRestWallet
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
CLightningWallet:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup Regtest
run: |
docker build -t 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
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 pylightning
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
env:
PYTHONUNBUFFERED: 1
PORT: 5123
LNBITS_DATA_FOLDER: ./data
LNBITS_BACKEND_WALLET_CLASS: CLightningWallet
CLIGHTNING_RPC: docker/data/clightning-1/regtest/lightning-rpc
run: |
sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data
make test-real-wallet
+65 -35
View File
@@ -3,15 +3,15 @@ name: tests
on: [push, pull_request]
jobs:
unit:
venv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
python-version: [3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -22,37 +22,67 @@ jobs:
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 requests trio mock
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run tests
run: make test
# build:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# python-version: [3.7, 3.8]
# steps:
# - uses: actions/checkout@v2
# - name: Set up Python ${{ matrix.python-version }}
# uses: actions/setup-python@v1
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r requirements.txt
# - name: Test with pytest
# env:
# LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
# LNBITS_FORCE_HTTPS: 0
# LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/
# LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
# LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
# LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
# LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
# run: |
# pip install pytest pytest-cov
# pytest --cov=lnbits --cov-report=xml
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1
# with:
# file: ./coverage.xml
venv-postgres:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
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.8]
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
env:
LNBITS_DATABASE_URL: postgres://postgres:postgres@0.0.0.0:5432/postgres
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
pipenv-sqlite:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
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
run: |
pip install pipenv
pipenv install --dev
pipenv install importlib-metadata
- name: Run tests
run: make test-pipenv
+6 -1
View File
@@ -15,7 +15,7 @@ __pycache__
.webassets-cache
htmlcov
test-reports
tests/data
tests/data/*.sqlite3
*.swo
*.swp
@@ -31,5 +31,10 @@ venv
__bundle__
coverage.xml
node_modules
lnbits/static/bundle.*
docker
# Nix
*result*
+6 -2
View File
@@ -8,8 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install build deps
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential
RUN apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev
RUN python -m pip install --upgrade pip
RUN pip install wheel
# Install runtime deps
COPY requirements.txt /tmp/requirements.txt
@@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /app
COPY --chown=1000:1000 lnbits /app/lnbits
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["uvicorn", "lnbits.__main__:app", "--port", "5000", "--host", "0.0.0.0"]
CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Arc
Copyright (c) 2022 Arc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+27 -3
View File
@@ -2,7 +2,7 @@
all: format check requirements.txt
format: prettier black
format: prettier isort black
check: mypy checkprettier checkblack
@@ -17,12 +17,18 @@ mypy: $(shell find lnbits -name "*.py")
./venv/bin/mypy lnbits/core
./venv/bin/mypy lnbits/extensions/*
isort: $(shell find lnbits -name "*.py")
./venv/bin/isort --profile black lnbits
checkprettier: $(shell find lnbits -name "*.js" -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
checkblack: $(shell find lnbits -name "*.py")
./venv/bin/black --check lnbits
checkisort: $(shell find lnbits -name "*.py")
./venv/bin/isort --profile black --check-only lnbits
Pipfile.lock: Pipfile
./venv/bin/pipenv lock
@@ -30,8 +36,26 @@ requirements.txt: Pipfile.lock
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt
test:
rm -rf ./tests/data
mkdir -p ./tests/data
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-real-wallet:
mkdir -p ./tests/data
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
./venv/bin/pytest -s
./venv/bin/pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
test-pipenv:
mkdir -p ./tests/data
LNBITS_BACKEND_WALLET_CLASS="FakeWallet" \
FAKE_WALLET_SECRET="ToTheMoon1" \
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
pipenv run pytest --durations=1 -s --cov=lnbits --cov-report=xml tests
bak:
# LNBITS_DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/postgres
+10 -3
View File
@@ -4,7 +4,7 @@ url = "https://pypi.org/simple"
verify_ssl = true
[requires]
python_version = "3.7"
python_version = "3.8"
[packages]
bitstring = "*"
@@ -12,6 +12,7 @@ cerberus = "*"
ecdsa = "*"
environs = "*"
lnurl = "==0.3.6"
loguru = "*"
pyscss = "*"
shortuuid = "*"
typing-extensions = "*"
@@ -27,11 +28,17 @@ asyncio = "*"
fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*"
jinja2 = "3.0.1"
jinja2 = "==3.0.1"
pyngrok = "*"
secp256k1 = "==0.14.0"
cffi = "==1.15.0"
pycryptodomex = "*"
[dev-packages]
black = "==20.8b1"
pytest = "*"
pytest-cov = "*"
mypy = "latest"
mypy = "*"
pytest-asyncio = "*"
requests = "*"
mock = "*"
Generated
+697 -438
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
web: hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
+4 -9
View File
@@ -1,9 +1,6 @@
LNbits
======
[![github-tests-badge]][github-tests]
[![github-mypy-badge]][github-mypy]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE)
[![docs-badge]][docs]
@@ -14,7 +11,9 @@ LNbits
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
Use [legend.lnbits.com](https://legend.lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
@@ -36,10 +35,6 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
See the [install guide](docs/guide/installation.md) for details on installation and setup.
### Contributing to LNbits
There's a [slightly different setup](docs/devs/installation.md) if you want to contribute to LNbits, but if your changes don't require adding or removing any package dependencies you don't have to bother with that, just follow the [normal installation](docs/guide/installation.md) steps.
## LNbits as an account system
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv + more to come..
@@ -72,7 +67,7 @@ Wallets can be easily generated and given out to people at events (one click mul
## Tip us
If you like this project and might even use or extend it, why not [send some tip love](https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
If you like this project and might even use or extend it, why not [send some tip love](https://legend.lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK)!
[docs]: https://lnbits.org/
-7
View File
@@ -1,7 +0,0 @@
{
"scripts": {
"dokku": {
"predeploy": "quart migrate && quart assets"
}
}
}
+110
View File
@@ -0,0 +1,110 @@
import warnings
import subprocess
import glob
import os
from os import path
from typing import Any, List, NamedTuple, Optional
from pathlib import Path
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()
# root = Path("lnbits/static/foo")
# root.mkdir(parents=True)
# root.joinpath("example.css").write_text("")
if __name__ == "__main__":
build()
#def build(setup_kwargs):
# """Build """
# transpile_scss()
# bundle_vendored()
# subprocess.run(["ls", "-la", "./lnbits/static"])
+1 -1
View File
@@ -17,7 +17,7 @@ Tests
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
```bash
./venv/bin/pip install pytest pytest-asyncio requests trio mock
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
```
Then to run the tests:
+1
View File
@@ -15,6 +15,7 @@ cp lnbits/extensions/example lnbits/extensions/mysuperplugin -r # Let's not use
cd lnbits/extensions/mysuperplugin
find . -type f -print0 | xargs -0 sed -i 's/example/mysuperplugin/g' # Change all occurrences of 'example' to your plugin name 'mysuperplugin'.
```
- 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:
* 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.
+5 -54
View File
@@ -7,59 +7,10 @@ nav_order: 1
# Installation
Download the latest stable release https://github.com/lnbits/lnbits/releases
This guide has been moved to the [installation guide](../guide/installation.md).
To install the developer packages, use `pipenv install --dev`.
## Application dependencies
## Notes:
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# for now you'll need to `git checkout FastAPI`
pipenv shell
pipenv install --dev
If any of the modules fails to install, try checking and upgrading your setupTool module.
`pip install -U setuptools`
If you wish to use a version of Python higher than 3.7:
pipenv --python 3.8 install --dev
You will need to copy `.env.example` to `.env`, then set variables there.
![Files](https://i.imgur.com/ri2zOe8.png)
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `pipenv run pip install purerpc`.
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
## Running the server
LNbits uses [FastAPI][fastapi] as an application server.
Before running the server for the first time, make sure to create the data folder and edit the .env file:
mkdir data
cp .env.example .env
sudo nano .env
To then run the server, use:
pipenv run python -m uvicorn lnbits.__main__:app --reload
**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run:
./nrok http 5000
this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits.
## Frontend
The frontend uses [Vue.js and Quasar][quasar].
[quart]: https://pgjones.gitlab.io/
[pipenv]: https://pipenv.pypa.io/
[polar]: https://lightningpolar.com/
[quasar]: https://quasar.dev/start/how-to-use-vue
* We recommend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
* <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session.
+218 -37
View File
@@ -4,8 +4,126 @@ title: Basic installation
nav_order: 2
---
# Basic installation
Install Postgres and setup a database for LNbits:
You can choose between four package managers, `poetry`, `pipenv`, `venv` 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: poetry
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
curl -sSL https://install.python-poetry.org | python3 -
poetry install
# You may need to install python 3.9, update your python following this guide https://linuxize.com/post/how-to-install-python-3-9-on-ubuntu-20-04/
mkdir data && cp .env.example .env
```
#### Running the server
```sh
poetry run lnbits
# To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0'
```
## Option 2: pipenv
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
sudo apt update && sudo apt install -y pipenv
pipenv install --dev
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools wheel
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
mkdir data && cp .env.example .env
```
#### Running the server
```sh
pipenv run python -m uvicorn lnbits.__main__:app --port 5000 --host 0.0.0.0
```
Add the flag `--reload` for development (includes hot-reload).
## Option 3: venv
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv'
python3 -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
```
#### 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: Nix
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
# Install nix, modern debian distros usually already include
sh <(curl -L https://nixos.org/nix/install) --daemon
nix build .#lnbits
mkdir data
```
#### Running the server
```sh
# .env variables are currently passed when running
LNBITS_DATA_FOLDER=data LNBITS_BACKEND_WALLET_CLASS=LNbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=7b1a78d6c78f48b09a202f2dcb2d22eb ./result/bin/lnbits --port 9000
```
### Troubleshooting
Problems installing? These commands have helped us install LNbits.
```sh
sudo apt install pkg-config libffi-dev libpq-dev
# if the secp256k1 build fails:
# if you used pipenv (option 1)
pipenv install setuptools wheel
# if you used venv (option 2)
./venv/bin/pip install setuptools wheel
# build essentials for debian/ubuntu
sudo apt install python3-dev gcc build-essential
```
### 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:
```sh
# on debian/ubuntu 'sudo apt-get -y install postgresql'
# or follow instructions at https://www.postgresql.org/download/linux/
@@ -21,57 +139,126 @@ createdb lnbits
exit
```
Download this repo and install the dependencies:
You need to edit the `.env` file.
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
# for now you'll need to `git checkout FastAPI`
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
# postgres://<user>:<myPassword>@<host>/<lnbits> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
./venv/bin/uvicorn lnbits.__main__:app --port 5000
```
# Using LNbits
Now you can visit your LNbits at http://localhost:5000/.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
Then you can restart it and it will be using the new settings.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
## Important note
If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres!
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above.
```sh
# STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
python3 conv.py
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
# Additional guides
### LNbits running on Umbrel behind Tor
## SQLite to PostgreSQL migration
If you already have LNbits installed and running, on an SQLite database, we **highly** recommend you migrate to postgres if you are planning to run LNbits on scale.
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user (see Postgres install guide above). Additionally, your LNbits instance should run once on postgres to implement the database schema before the migration works:
```sh
# STOP LNbits
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
# START LNbits
# STOP LNbits
# on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials
python3 tools/conv.py
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
## LNbits as a systemd service
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
```
# Systemd unit for lnbits
# /etc/systemd/system/lnbits.service
[Unit]
Description=LNbits
# you can uncomment these lines if you know what you're doing
# it will make sure that lnbits starts after lnd (replace with your own backend service)
#Wants=lnd.service
#After=lnd.service
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/bitcoin/lnbits
# same here
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000
# replace with the user that you're running lnbits on
User=bitcoin
Restart=always
TimeoutSec=120
RestartSec=30
# this makes sure that you receive logs in real time
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
```
Save the file and run the following commands:
```sh
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## 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.
#### Install mkcert
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
Install mkcert on Ubuntu:
```sh
sudo apt install libnss3-tools
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
```
#### Create certificate
To create a certificate, first `cd` into your lnbits folder and execute the following command ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/))
```sh
# add your local IP (192.x.x.x) as well if you want to use it in your local network
mkcert localhost 127.0.0.1 ::1
```
This will create two new files (`localhost-key.pem` and `localhost.pem `) which you can then pass to uvicorn when you start LNbits:
```sh
./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./localhost-key.pem --ssl-certfile ./localhost.pem
```
## LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
### Docker installation
## Docker installation
To install using docker you first need to build the docker image as:
@@ -103,9 +290,3 @@ docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/
```
Finally you can access your lnbits on your machine at port 5000.
# Additional guides
## LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
+11 -3
View File
@@ -35,16 +35,24 @@ Using this wallet requires the installation of the `grpcio` and `protobuf` Pytho
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/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`.
### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
- `LND_REST_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
or
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
### LNbits
Generated
+77
View File
@@ -0,0 +1,77 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1657114324,
"narHash": "sha256-fWuaUNXrHcz/ciHRHlcSO92dvV3EVS0GJQUSBO5JIB4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a5c867d9fe9e4380452628e8f171c26b69fa9d3d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"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"
},
"locked": {
"lastModified": 1657149754,
"narHash": "sha256-iSnZoqwNDDVoO175whSuvl4sS9lAb/2zZ3Sa4ywo970=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "fc1930e011dea149db81863aac22fe701f36f1b5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
}
},
"root": "root",
"version": 7
}
+55
View File
@@ -0,0 +1,55 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
poetry2nix.url = "github:nix-community/poetry2nix";
};
outputs = { self, nixpkgs, poetry2nix }@inputs:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
forSystems = systems: f:
nixpkgs.lib.genAttrs systems
(system: f system (import nixpkgs { inherit system; overlays = [ poetry2nix.overlay self.overlays.default ]; }));
forAllSystems = forSystems supportedSystems;
projectName = "lnbits";
in
{
devShells = forAllSystems (system: pkgs: {
default = pkgs.mkShell {
buildInputs = with pkgs; [
nodePackages.prettier
];
};
});
overlays = {
default = final: prev: {
${projectName} = self.packages.${final.hostPlatform.system}.${projectName};
};
};
packages = forAllSystems (system: pkgs: {
default = self.packages.${system}.${projectName};
${projectName} = pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
python = pkgs.python39;
};
});
nixosModules = {
default = { pkgs, lib, config, ... }: {
imports = [ "${./nix/modules/${projectName}-service.nix}" ];
nixpkgs.overlays = [ self.overlays.default ];
};
};
checks = forAllSystems (system: pkgs:
let
vmTests = import ./nix/tests {
makeTest = (import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest;
inherit inputs pkgs;
};
in
pkgs.lib.optionalAttrs pkgs.stdenv.isLinux vmTests # vmTests can only be ran on Linux, so append them only if on Linux.
//
{
# Other checks here...
}
);
};
}
+15 -13
View File
@@ -1,36 +1,38 @@
import asyncio
import uvloop
from loguru import logger
from starlette.requests import Request
from .commands import bundle_vendored, migrate_databases, transpile_scss
from .commands import migrate_databases
from .settings import (
DEBUG,
HOST,
LNBITS_COMMIT,
LNBITS_DATA_FOLDER,
LNBITS_DATABASE_URL,
LNBITS_SITE_TITLE,
PORT,
SERVICE_FEE,
WALLET,
)
uvloop.install()
asyncio.create_task(migrate_databases())
transpile_scss()
bundle_vendored()
from .app import create_app
app = create_app()
print(
f"""Starting LNbits with
- git version: {LNBITS_COMMIT}
- site title: {LNBITS_SITE_TITLE}
- debug: {DEBUG}
- data folder: {LNBITS_DATA_FOLDER}
- funding source: {WALLET.__class__.__name__}
- service fee: {SERVICE_FEE}
"""
logger.info("Starting LNbits")
logger.info(f"Host: {HOST}")
logger.info(f"Port: {PORT}")
logger.info(f"Debug: {DEBUG}")
logger.info(f"Site title: {LNBITS_SITE_TITLE}")
logger.info(f"Funding source: {WALLET.__class__.__name__}")
logger.info(
f"Database: {'PostgreSQL' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('postgres://') else 'CockroachDB' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('cockroachdb://') else 'SQLite'}"
)
logger.info(f"Data folder: {LNBITS_DATA_FOLDER}")
logger.info(f"Git version: {LNBITS_COMMIT}")
# logger.info(f"Service fee: {SERVICE_FEE}")
+92 -32
View File
@@ -1,17 +1,21 @@
import asyncio
import importlib
from lnbits.core.tasks import register_task_listeners
import logging
import sys
import traceback
import warnings
from http import HTTPStatus
from fastapi import FastAPI, 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
import lnbits.settings
from lnbits.core.tasks import register_task_listeners
from .commands import db_migrate, handle_assets
from .core import core_app
@@ -39,20 +43,27 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
"""Create application factory.
:param config_object: The configuration object to use.
"""
app = FastAPI()
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static")
configure_logger()
app = FastAPI(
title="LNbits API",
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
license_info={
"name": "MIT License",
"url": "https://raw.githubusercontent.com/lnbits/lnbits-legend/main/LICENSE",
},
)
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
app.mount(
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static"
"/core/static",
StaticFiles(packages=[("lnbits.core", "static")]),
name="core_static",
)
origins = ["http://localhost", "http://localhost:5000"]
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
CORSMiddleware, allow_origins=origins, allow_methods=["*"], allow_headers=["*"]
)
g().config = lnbits.settings
@@ -62,15 +73,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
return template_renderer().TemplateResponse(
"error.html",
{"request": request, "err": f"`{exc.errors()}` is not a valid UUID."},
)
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
# return HTMLResponse(
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
# )
if "text/html" in request.headers["accept"]:
return template_renderer().TemplateResponse(
"error.html",
{"request": request, "err": f"{exc.errors()} is not a valid UUID."},
)
return JSONResponse(
status_code=HTTPStatus.NO_CONTENT,
content={"detail": exc.errors()},
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
# app.add_middleware(ASGIProxyFix)
@@ -88,18 +103,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
def check_funding_source(app: FastAPI) -> None:
@app.on_event("startup")
async def check_wallet_status():
error_message, balance = await WALLET.status()
if error_message:
warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
while True:
error_message, balance = await WALLET.status()
if not error_message:
break
logger.error(
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning,
)
sys.exit(4)
else:
print(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
)
logger.info("Retrying connection to backend in 5 seconds...")
await asyncio.sleep(5)
logger.info(
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
)
def register_routes(app: FastAPI) -> None:
@@ -121,9 +137,10 @@ def register_routes(app: FastAPI) -> None:
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)
except Exception as e:
print(str(e))
logger.error(str(e))
raise ImportError(
f"Please make sure that the extension `{ext.code}` follows conventions."
)
@@ -170,10 +187,53 @@ def register_async_tasks(app):
def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception)
async def basic_error(request: Request, err):
print("handled error", traceback.format_exc())
logger.error("handled error", traceback.format_exc())
logger.error("ERROR:", err)
etype, _, tb = sys.exc_info()
traceback.print_exception(etype, err, tb)
exc = traceback.format_exc()
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err}
if "text/html" in request.headers["accept"]:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err}
)
return JSONResponse(
status_code=HTTPStatus.NO_CONTENT,
content={"detail": err},
)
def configure_logger() -> None:
logger.remove()
log_level: str = "DEBUG" if lnbits.settings.DEBUG else "INFO"
formatter = Formatter()
logger.add(sys.stderr, level=log_level, format=formatter.format)
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
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"
if lnbits.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"
else:
self.fmt: str = self.minimal_fmt
def format(self, record):
function = "{function}".format(**record)
if function == "emit": # uvicorn logs
return self.minimal_fmt
return self.fmt
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
logger.log(level, record.getMessage())
+207 -5
View File
@@ -1,11 +1,16 @@
import bitstring # type: ignore
import re
import hashlib
import re
import time
from binascii import unhexlify
from decimal import Decimal
from typing import List, NamedTuple, Optional
from bech32 import bech32_decode, CHARSET # type: ignore
import bitstring # type: ignore
import embit
import secp256k1
from bech32 import CHARSET, bech32_decode, bech32_encode
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify
class Route(NamedTuple):
@@ -116,6 +121,166 @@ def decode(pr: str) -> Invoice:
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 = unhexlify(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(
(
unhexlify(splits[0]),
unhexlify(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", hashlib.sha256(v.encode("utf-8")).digest())
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(unhexlify(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(
hexlify(self.pubkey.serialize()).decode("utf-8"),
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:
@@ -125,7 +290,7 @@ def _unshorten_amount(amount: str) -> int:
# * `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}
units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
unit = str(amount)[-1]
# BOLT #11:
@@ -146,6 +311,34 @@ def _pull_tagged(stream):
return (CHARSET[tag], stream.read(length * 5), stream)
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray
def tagged(char, l):
# 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()
@@ -167,3 +360,12 @@ def _u5_to_bitarray(arr: List[int]) -> 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
+11 -8
View File
@@ -1,16 +1,19 @@
import asyncio
import warnings
import click
import importlib
import re
import os
import re
import warnings
from .db import SQLITE, POSTGRES, COCKROACH
from .core import db as core_db, migrations as core_migrations
import click
from loguru import logger
from .core import db as core_db
from .core import migrations as core_migrations
from .db import COCKROACH, POSTGRES, SQLITE
from .helpers import (
get_valid_extensions,
get_css_vendored,
get_js_vendored,
get_valid_extensions,
url_for_vendored,
)
from .settings import LNBITS_PATH
@@ -69,7 +72,7 @@ async def migrate_databases():
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
logger.debug(f"running migration {db_name}.{version}")
await migrate(db)
if db.schema == None:
@@ -110,4 +113,4 @@ async def migrate_databases():
async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations)
print(" ✔️ All migrations done.")
logger.info("✔️ All migrations done.")
+40 -13
View File
@@ -1,16 +1,15 @@
import json
import datetime
from uuid import uuid4
from typing import List, Optional, Dict, Any
import json
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
from uuid import uuid4
from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME
from lnbits.db import COCKROACH, POSTGRES, Connection
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db
from .models import User, Wallet, Payment, BalanceCheck
from .models import BalanceCheck, Payment, User, Wallet
# accounts
# --------
@@ -62,6 +61,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
email=user["email"],
extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets],
admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
if LNBITS_ADMIN_USERS
else False,
)
@@ -178,16 +180,28 @@ async def get_wallet_for_key(
async def get_standalone_payment(
checking_id_or_hash: str, conn: Optional[Connection] = None
checking_id_or_hash: str,
conn: Optional[Connection] = None,
incoming: Optional[bool] = False,
wallet_id: Optional[str] = None,
) -> Optional[Payment]:
clause: str = "checking_id = ? OR hash = ?"
values = [checking_id_or_hash, checking_id_or_hash]
if incoming:
clause = f"({clause}) AND amount > 0"
if wallet_id:
clause = f"({clause}) AND wallet = ?"
values.append(wallet_id)
row = await (conn or db).fetchone(
"""
f"""
SELECT *
FROM apipayments
WHERE checking_id = ? OR hash = ?
WHERE {clause}
LIMIT 1
""",
(checking_id_or_hash, checking_id_or_hash),
tuple(values),
)
return Payment.from_row(row) if row else None
@@ -217,6 +231,8 @@ async def get_payments(
incoming: bool = False,
since: Optional[int] = None,
exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
conn: Optional[Connection] = None,
) -> List[Payment]:
"""
@@ -261,6 +277,15 @@ 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
)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
@@ -271,14 +296,16 @@ async def get_payments(
FROM apipayments
{where}
ORDER BY time DESC
{limit_offset_clause}
""",
tuple(args),
)
return [Payment.from_row(row) for row in rows]
async def delete_expired_invoices(conn: Optional[Connection] = None,) -> None:
async def delete_expired_invoices(
conn: Optional[Connection] = None,
) -> None:
# first we delete all invoices older than one month
await (conn or db).execute(
f"""
+13 -7
View File
@@ -1,12 +1,15 @@
import json
import hmac
import hashlib
from lnbits.helpers import url_for
import hmac
import json
from sqlite3 import Row
from typing import Dict, List, NamedTuple, Optional
from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
from loguru import logger
from pydantic import BaseModel
from lnbits.helpers import url_for
from lnbits.settings import WALLET
@@ -57,6 +60,7 @@ class User(BaseModel):
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
admin: bool = False
@property
def wallet_ids(self) -> List[str]:
@@ -141,10 +145,12 @@ class Payment(BaseModel):
status = await WALLET.get_invoice_status(self.checking_id)
if self.is_out and status.failed:
print(f" - deleting outgoing failed payment {self.checking_id}: {status}")
logger.info(
f" - deleting outgoing failed payment {self.checking_id}: {status}"
)
await self.delete()
elif not status.pending:
print(
logger.info(
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
)
await self.set_pending(status.pending)
+49 -22
View File
@@ -6,14 +6,22 @@ from typing import Dict, Optional, Tuple
from urllib.parse import parse_qs, urlparse
import httpx
from fastapi import Depends
from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl # type: ignore
from loguru import logger
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import WALLET
from lnbits.settings import FAKE_WALLET, WALLET
from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db
@@ -48,15 +56,19 @@ async def create_invoice(
description_hash: Optional[bytes] = None,
extra: Optional[Dict] = None,
webhook: Optional[str] = None,
internal: Optional[bool] = False,
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
# use the fake wallet if the invoice is for internal use only
wallet = FAKE_WALLET if internal else WALLET
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
amount=amount, memo=invoice_memo, description_hash=description_hash
)
if not ok:
raise InvoiceFailure(error_message or "Unexpected backend error.")
raise InvoiceFailure(error_message or "unexpected backend error.")
invoice = bolt11.decode(payment_request)
@@ -85,20 +97,17 @@ async def pay_invoice(
description: str = "",
conn: Optional[Connection] = None,
) -> str:
invoice = bolt11.decode(payment_request)
fee_reserve_msat = fee_reserve(invoice.amount_msat)
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11.decode(payment_request)
if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.")
wallet = await get_wallet(wallet_id, conn=conn)
if invoice.amount_msat > wallet.balance_msat - (wallet.balance_msat / 100 * 2):
raise PermissionError("LNbits requires you keep at least 2% reserve to cover potential routing fees.")
# put all parameters that don't change here
PaymentKwargs = TypedDict(
"PaymentKwargs",
@@ -123,6 +132,7 @@ async def pay_invoice(
# check_internal() returns the checking_id of the invoice we're waiting for
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
if internal_checking_id:
logger.debug(f"creating temporary internal payment with id {internal_id}")
# create a new payment from this wallet
await create_payment(
checking_id=internal_id,
@@ -132,11 +142,12 @@ async def pay_invoice(
**payment_kwargs,
)
else:
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(invoice.amount_msat),
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
@@ -145,9 +156,15 @@ async def pay_invoice(
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
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 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
)
raise PermissionError("Insufficient balance.")
if internal_checking_id:
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
@@ -162,9 +179,14 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id)
else:
logger.debug(f"backend: sending payment {temp_id}")
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat
)
logger.debug(f"backend: pay_invoice finished {temp_id}")
if payment.checking_id:
logger.debug(f"creating final payment {payment.checking_id}")
async with db.connect() as conn:
await create_payment(
checking_id=payment.checking_id,
@@ -174,15 +196,18 @@ async def pay_invoice(
conn=conn,
**payment_kwargs,
)
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
else:
logger.debug(f"backend payment failed, no checking_id {temp_id}")
async with db.connect() as conn:
logger.debug(f"deleting temporary payment {temp_id}")
await delete_payment(temp_id, conn=conn)
raise PaymentFailure(
payment.error_message
or "Payment failed, but backend didn't give us an error message."
)
logger.debug(f"payment successful {payment.checking_id}")
return invoice.payment_hash
@@ -213,7 +238,7 @@ async def redeem_lnurl_withdraw(
conn=conn,
)
except:
print(
logger.warning(
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
)
return None
@@ -240,12 +265,14 @@ async def redeem_lnurl_withdraw(
async def perform_lnurlauth(
callback: str, conn: Optional[Connection] = None
callback: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
conn: Optional[Connection] = None,
) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g().wallet.lnurlauth_key(cb.netloc)
key = wallet.wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks"""
@@ -278,12 +305,12 @@ async def perform_lnurlauth(
sign_len = 6 + r_len + s_len
signature = BytesIO()
signature.write(0x30 .to_bytes(1, "big", signed=False))
signature.write(0x30.to_bytes(1, "big", signed=False))
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02 .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(0x02 .to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
@@ -322,16 +349,16 @@ async def check_invoice_status(
if not payment.pending:
return status
if payment.is_out and status.failed:
print(
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete()
elif not status.pending:
print(
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
logger.info(
f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
)
await payment.set_pending(status.pending)
return status
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int) -> int:
return max(1000, int(amount_msat * 0.01))
return max(2000, int(amount_msat * 0.01))
+32
View File
@@ -1,4 +1,36 @@
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]
})
+51
View File
@@ -0,0 +1,51 @@
// the cache version gets updated every time there is a new deployment
const CACHE_VERSION = 1
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
const getApiKey = request => {
return request.headers.get('X-Api-Key') || 'none'
}
// on activation we clean up the previously registered service workers
self.addEventListener('activate', evt =>
evt.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
if (currentCacheVersion !== CACHE_VERSION) {
return caches.delete(cacheName)
}
})
)
})
)
)
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (
event.request.url.startsWith(self.location.origin) &&
event.request.method == 'GET'
) {
// Open the cache
event.respondWith(
caches.open(CURRENT_CACHE + getApiKey(event.request)).then(cache => {
// Go to the network first
return fetch(event.request)
.then(fetchedResponse => {
cache.put(event.request, fetchedResponse.clone())
return fetchedResponse
})
.catch(() => {
// If the network is unavailable, get
return cache.match(event.request.url)
})
})
)
}
})
+46 -12
View File
@@ -161,14 +161,14 @@ new Vue({
{
name: 'sat',
align: 'right',
label: 'Amount (sat)',
label: 'Amount (' + LNBITS_DENOMINATION + ')',
field: 'sat',
sortable: true
},
{
name: 'fee',
align: 'right',
label: 'Fee (msat)',
label: 'Fee (m' + LNBITS_DENOMINATION + ')',
field: 'fee'
}
],
@@ -185,12 +185,17 @@ new Vue({
location: window.location
},
balance: 0,
credit: 0,
newName: ''
}
},
computed: {
formattedBalance: function () {
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
if (LNBITS_DENOMINATION != 'sats') {
return this.balance / 100
} else {
return LNbits.utils.formatSat(this.balance || this.g.wallet.sat)
}
},
filteredPayments: function () {
var q = this.paymentsTable.filter
@@ -249,6 +254,28 @@ new Vue({
this.parse.data.paymentChecker = null
this.parse.camera.show = false
},
updateBalance: function (credit) {
if (LNBITS_DENOMINATION != 'sats') {
credit = credit * 100
}
LNbits.api
.request('PUT', '/api/v1/wallet/balance/' + credit, this.g.wallet.inkey)
.catch(err => {
LNbits.utils.notifyApiError(err)
})
.then(response => {
let data = response.data
if (data.status === 'ERROR') {
this.$q.notify({
timeout: 5000,
type: 'warning',
message: `Failed to update.`
})
return
}
this.balance = this.balance + data.balance
})
},
closeReceiveDialog: function () {
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
@@ -271,7 +298,9 @@ new Vue({
},
createInvoice: function () {
this.receive.status = 'loading'
if (LNBITS_DENOMINATION != 'sats') {
this.receive.data.amount = this.receive.data.amount * 100
}
LNbits.api
.createInvoice(
this.g.wallet,
@@ -335,12 +364,12 @@ new Vue({
},
decodeRequest: function () {
this.parse.show = true
let req = this.parse.data.request.toLowerCase()
if (this.parse.data.request.startsWith('lightning:')) {
this.parse.data.request = this.parse.data.request.slice(10)
} else if (this.parse.data.request.startsWith('lnurl:')) {
this.parse.data.request = this.parse.data.request.slice(6)
} else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) {
} else if (req.indexOf('lightning=lnurl1') !== -1) {
this.parse.data.request = this.parse.data.request
.split('lightning=')[1]
.split('&')[0]
@@ -589,10 +618,10 @@ new Vue({
},
updateWalletName: function () {
let newName = this.newName
let adminkey = this.g.wallet.adminkey
if (!newName || !newName.length) return
// let data = {name: newName}
LNbits.api
.request('PUT', '/api/v1/wallet/' + newName, this.g.wallet.inkey, {})
.request('PUT', '/api/v1/wallet/' + newName, adminkey, {})
.then(res => {
this.newName = ''
this.$q.notify({
@@ -662,10 +691,7 @@ new Vue({
},
mounted: function () {
// show disclaimer
if (
this.$refs.disclaimer &&
!this.$q.localStorage.getItem('lnbits.disclaimerShown')
) {
if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) {
this.disclaimerDialog.show = true
this.$q.localStorage.set('lnbits.disclaimerShown', true)
}
@@ -676,3 +702,11 @@ new Vue({
)
}
})
if (navigator.serviceWorker != null) {
navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
console.log('Registered events at scope: ', registration.scope)
})
}
+7 -4
View File
@@ -1,7 +1,9 @@
import asyncio
import httpx
from typing import List
import httpx
from loguru import logger
from lnbits.tasks import register_invoice_listener
from . import db
@@ -20,7 +22,7 @@ async def register_task_listeners():
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
while True:
payment = await invoice_paid_queue.get()
logger.debug("received invoice paid event")
# send information to sse channel
await dispatch_invoice_listener(payment)
@@ -44,14 +46,15 @@ async def dispatch_invoice_listener(payment: Payment):
try:
send_channel.put_nowait(payment)
except asyncio.QueueFull:
print("removing sse listener", send_channel)
logger.debug("removing sse listener", send_channel)
api_invoice_listeners.remove(send_channel)
async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment._asdict()
data = payment.dict()
try:
logger.debug("sending webhook", payment.webhook)
r = await client.post(payment.webhook, json=data, timeout=40)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
+5 -3
View File
@@ -48,7 +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;}</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
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
@@ -61,8 +63,8 @@
<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;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code
&lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
+14 -1
View File
@@ -2,10 +2,23 @@
%} {% block scripts %} {{ window_vars(user) }}
<script src="/core/static/js/extensions.js"></script>
{% endblock %} {% 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">
<q-icon
v-if="searchTerm !== ''"
name="close"
@click="searchTerm = ''"
class="cursor-pointer q-mt-lg"
/>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md">
<div
class="col-6 col-md-4 col-lg-3"
v-for="extension in g.extensions"
v-for="extension in filteredExtensions"
:key="extension.code"
>
<q-card>
+1 -1
View File
@@ -11,7 +11,7 @@
color="primary"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
>
Press to claim bitcoin
</q-btn>
+496 -375
View File
@@ -4,7 +4,6 @@
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}
<script src="/core/static/js/wallet.js"></script>
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest" />
{% endblock %}
<!---->
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
@@ -15,10 +14,54 @@
<q-card>
<q-card-section>
<h3 class="q-my-none">
<strong>{% raw %}{{ formattedBalance }}{% endraw %}</strong> sat
<strong>{% raw %}{{ formattedBalance }} {% endraw %}</strong>
{{LNBITS_DENOMINATION}}
<q-btn
v-if="'{{user.admin}}' == 'True'"
flat
round
color="primary"
icon="add"
size="md"
>
<q-popup-edit
class="bg-accent text-white"
v-slot="scope"
v-model="credit"
>
<q-input
v-if="'{{LNBITS_DENOMINATION}}' != 'sats'"
label="Amount to credit account"
v-model="scope.value"
dense
autofocus
mask="#.##"
fill-mask="0"
reverse-fill-mask
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
<q-input
v-else
type="number"
label="Amount to credit account"
v-model="scope.value"
dense
autofocus
@keyup.enter="updateBalance(scope.value)"
>
<template v-slot:append>
<q-icon name="edit" />
</template>
</q-input>
</q-popup-edit>
</q-btn>
</h3>
</q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md">
<div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
<div class="col">
<q-btn
unelevated
@@ -141,7 +184,17 @@
<q-tooltip>{{ props.row.date }}</q-tooltip>
{{ props.row.dateFrom }}
</q-td>
<q-td auto-width key="sat" :props="props">
{% endraw %}
<q-td
auto-width
key="sat"
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">
@@ -219,401 +272,469 @@
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "core/_api_docs.html" %}
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
{% else %}
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %}
<q-expansion-item group="extras" icon="crop_free" label="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}}">
<q-list>
{% include "core/_api_docs.html" %}
<q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %}
<q-expansion-item
group="extras"
icon="crop_free"
label="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="{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep pulling
the funds continuously from here after the first withdraw.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% endif %}
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
>
<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>
<qrcode
value="{{wallet.lnurlwithdraw_full}}"
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep pulling the
funds continuously from here after the first withdraw.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% endif %}
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
>
<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>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
<q-card>
<q-card-section>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="newName"
label="Label"
dense="dense"
@update:model-value="(e) => console.log(e)"
/>
</div>
<q-btn
:disable="!newName.length"
unelevated
class="q-mt-sm"
color="primary"
@click="updateWalletName()"
>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"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
<q-card>
<q-card-section>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="newName"
label="Label"
dense="dense"
@update:model-value="(e) => console.log(e)"
/>
</div>
<q-btn
:disable="!newName.length"
unelevated
class="q-mt-sm"
color="primary"
@click="updateWalletName()"
>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"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
ADS.split(';') %}
<q-card>
<a href="{{ AD[0] }}"
><img width="100%" src="{{ AD[1] }}"
/></a> </q-card
>{% endfor %} {% endif %}
</div>
</div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %}
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
type="number"
:label="`Amount (${receive.unit}) *`"
:step="receive.unit != 'sat' ? '0.001' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo *"
placeholder="LNbits invoice"
></q-input>
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></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>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
{% raw %}
<h6 class="q-my-none">{{ parse.invoice.fsat }} sat</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</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>
</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>
</div>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %}
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be shared
with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</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
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} sat
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b> sat
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount (sat) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Send satoshis</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#.##"
fill-mask="0"
reverse-fill-mask
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
:step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="parse.data.request == ''"
:disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>Read</q-btn
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></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>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</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
>
</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
>
</div>
</q-form>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be
shared with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</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
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }}
{{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<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
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
<q-input
filled
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
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
>
</div>
</q-form>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
</div>
</div>
</div>
</div>
</q-card>
</q-dialog>
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<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-max"
active-class="px-0"
indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
{% if service_fee > 0 %}
<div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6>
<p>
Login functionality to be released in v0.2, 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. 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.
</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
>
</div>
</q-card>
</q-dialog>
{% endif %} {% endblock %}
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</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 v0.2, 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>
<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
>
</div>
</q-card>
</q-dialog>
{% endblock %}
</div>
+182 -59
View File
@@ -3,35 +3,43 @@ import hashlib
import json
from binascii import unhexlify
from http import HTTPStatus
from typing import Dict, Optional, Union
from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx
from fastapi import Query, Request
from fastapi import Depends, Header, Query, Request
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
from loguru import logger
from pydantic import BaseModel
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
from lnbits import bolt11, lnurl
from lnbits.bolt11 import Invoice
from lnbits.core.models import Payment, Wallet
from lnbits.decorators import (
WalletAdminKeyChecker,
WalletInvoiceKeyChecker,
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from lnbits.helpers import url_for
from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
from .. import core_app, db
from ..crud import (
create_payment,
get_payments,
get_standalone_payment,
get_wallet,
get_wallet_for_key,
save_balance_check,
update_payment_status,
update_wallet,
)
from ..services import (
@@ -57,9 +65,38 @@ 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/balance/{amount}")
async def api_update_balance(
amount: int, wallet: WalletTypeInfo = Depends(get_key_type)
):
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
)
payHash = urlsafe_short_hash()
await create_payment(
wallet_id=wallet.wallet.id,
checking_id=payHash,
payment_request="selfPay",
payment_hash=payHash,
amount=amount * 1000,
memo="selfPay",
fee=0,
)
await update_payment_status(checking_id=payHash, pending=False)
updatedWallet = await get_wallet(wallet.wallet.id)
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": amount,
}
@core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
await update_wallet(wallet.wallet.id, new_name)
return {
@@ -70,26 +107,42 @@ async def api_update_wallet(
@core_app.get("/api/v1/payments")
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
pendingPayments = await get_payments(wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True)
async def api_payments(
limit: Optional[int] = None,
offset: Optional[int] = None,
wallet: WalletTypeInfo = Depends(get_key_type),
):
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
limit=limit,
offset=offset,
)
for payment in pendingPayments:
await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
limit=limit,
offset=offset,
)
class CreateInvoiceData(BaseModel):
out: Optional[bool] = True
amount: int = Query(None, ge=1)
memo: str = None
amount: float = Query(None, ge=0)
memo: Optional[str] = None
unit: Optional[str] = "sat"
description_hash: 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
@@ -99,10 +152,11 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
memo = ""
else:
description_hash = b""
memo = data.memo
memo = data.memo or LNBITS_SITE_TITLE
if data.unit == "sat":
amount = data.amount
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
@@ -115,6 +169,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
description_hash=description_hash,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
conn=conn,
)
except InvoiceFailure as e:
@@ -126,8 +181,11 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
lnurl_response: Union[None, bool, str] = None
if data.lnurl_callback:
if "lnurl_balance_check" in g().data:
save_balance_check(g().wallet.id, data.lnurl_balance_check)
if "lnurl_balance_check" in data:
assert (
data.lnurl_balance_check is not None
), "lnurl_balance_check is required"
save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
@@ -138,7 +196,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
"balanceNotify": url_for(
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True,
wal=g().wallet.id,
wal=wallet.id,
),
},
timeout=10,
@@ -189,13 +247,9 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(get_key_type),
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoiceData = Body(...),
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
raise HTTPException(
@@ -205,8 +259,14 @@ async def api_payments_create(
return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet
) # admin key
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
elif not invoiceData.out:
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Invoice (or Admin) key required.",
)
class CreateLNURLData(BaseModel):
@@ -219,7 +279,7 @@ class CreateLNURLData(BaseModel):
@core_app.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(get_key_type)
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
):
domain = urlparse(data.callback).netloc
@@ -245,17 +305,23 @@ async def api_payments_pay_lnurl(
detail=f"{domain} said: '{params.get('reason', '')}'",
)
if not params.get("pr"):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} did not return a payment request.",
)
invoice = bolt11.decode(params["pr"])
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}.",
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 description_hash == {data.description_hash}, got {invoice.description_hash}.",
)
extra = {}
@@ -264,7 +330,7 @@ async def api_payments_pay_lnurl(
extra["success_action"] = params["successAction"]
if data.comment:
extra["comment"] = data.comment
assert data.description is not None, "description is required"
payment_hash = await pay_invoice(
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
@@ -281,19 +347,20 @@ async def api_payments_pay_lnurl(
async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id
this_wallet_id = wallet.id
payment_queue = asyncio.Queue(0)
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
print("adding sse listener", payment_queue)
logger.debug("adding sse listener", payment_queue)
api_invoice_listeners.append(payment_queue)
send_queue = asyncio.Queue(0)
send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None:
while True:
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("payment receieved", payment)
await send_queue.put(("payment-received", payment))
asyncio.create_task(payment_received())
@@ -318,34 +385,59 @@ async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
return EventSourceResponse(
subscribe(request, wallet), ping=20, media_type="text/event-stream"
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
)
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash):
payment = await get_standalone_payment(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 = None
try:
if X_Api_Key.extra:
logger.warning("No key")
except:
wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet 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
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
elif not payment.pending:
if wallet and wallet.id == payment.wallet_id:
return {"paid": True, "preimage": payment.preimage, "details": payment}
return {"paid": True, "preimage": payment.preimage}
try:
await payment.check_pending()
except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
return {"paid": False}
if wallet and wallet.id == payment.wallet_id:
return {
"paid": not payment.pending,
"preimage": payment.preimage,
"details": payment,
}
return {"paid": not payment.pending, "preimage": payment.preimage}
@core_app.get(
"/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]
)
async def api_lnurlscan(code: str):
@core_app.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
@@ -373,17 +465,15 @@ async def api_lnurlscan(code: str):
params.update(kind="auth")
params.update(callback=url) # with k1 already in it
lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update(
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
r = await client.get(url, timeout=5)
if r.is_error:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain,
"message": "failed to get parameters"},
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
@@ -413,8 +503,7 @@ async def api_lnurlscan(code: str):
if tag == "withdrawRequest":
params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"]
== data["maxWithdrawable"])
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
@@ -463,14 +552,19 @@ async def api_lnurlscan(code: str):
return params
class DecodePayment(BaseModel):
data: str
@core_app.post("/api/v1/payments/decode")
async def api_payments_decode(data: str = Query(None)):
async def api_payments_decode(data: DecodePayment):
payment_str = data.data
try:
if data["data"][:5] == "LNURL":
url = lnurl.decode(data["data"])
if payment_str[:5] == "LNURL":
url = lnurl.decode(payment_str)
return {"domain": url}
else:
invoice = bolt11.decode(data["data"])
invoice = bolt11.decode(payment_str)
return {
"payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat,
@@ -487,17 +581,46 @@ async def api_payments_decode(data: str = Query(None)):
return {"message": "Failed to decode"}
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback)
class Callback(BaseModel):
callback: str = Query(...)
@core_app.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(callback.callback, wallet=wallet)
if err:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
)
return ""
@core_app.get("/api/v1/currencies")
async def api_list_currencies_available():
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")
async def api_fiat_as_sats(data: ConversionData):
output = {}
if data.from_ == "sat":
output["sats"] = int(data.amount)
output["BTC"] = data.amount / 100000000
for currency in data.to.split(","):
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
return output
else:
output[data.from_.upper()] = data.amount
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
output["BTC"] = output["sats"] / 100000000
return output
+46 -13
View File
@@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException
from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse
@@ -14,8 +15,15 @@ from lnbits.core import db
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer, url_for
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, SERVICE_FEE
from lnbits.settings import (
LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
LNBITS_CUSTOM_LOGO,
LNBITS_SITE_TITLE,
SERVICE_FEE,
)
from ...helpers import get_valid_extensions
from ..crud import (
create_account,
create_wallet,
@@ -43,9 +51,7 @@ async def home(request: Request, lightning: str = None):
@core_html_routes.get(
"/extensions",
name="core.extensions",
response_class=HTMLResponse,
"/extensions", name="core.extensions", response_class=HTMLResponse
)
async def extensions(
request: Request,
@@ -61,11 +67,21 @@ async def extensions(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
# check if extension exists
if extension_to_enable or extension_to_disable:
ext = extension_to_enable or extension_to_disable
if ext not in [e.code for e in get_valid_extensions()]:
raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
)
if extension_to_enable:
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
)
elif extension_to_disable:
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
)
@@ -105,6 +121,7 @@ async def wallet(
if not user_id:
user = await get_user((await create_account()).id)
logger.info(f"Create user {user.id}")
else:
user = await get_user(user_id)
if not user:
@@ -115,17 +132,23 @@ async def wallet(
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User not authorized."}
)
if LNBITS_ADMIN_USERS and user_id in LNBITS_ADMIN_USERS:
user.admin = True
if not wallet_id:
if user.wallets and not wallet_name:
wallet = user.wallets[0]
else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
logger.info(
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
)
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
wallet = user.get_wallet(wallet_id)
if not wallet:
return template_renderer().TemplateResponse(
@@ -139,6 +162,7 @@ async def wallet(
"user": user.dict(),
"wallet": wallet.dict(),
"service_fee": service_fee,
"web_manifest": f"/manifest/{user.id}.webmanifest",
},
)
@@ -195,13 +219,13 @@ async def lnurl_full_withdraw_callback(request: Request):
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets]
print("USR", user_wallet_ids)
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)
user_wallet_ids.remove(wal)
logger.debug("Deleted wallet {wal} of user {user.id}")
if user_wallet_ids:
return RedirectResponse(
@@ -221,7 +245,9 @@ async def lnurl_balance_notify(request: Request, service: str):
redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse)
@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)
@@ -244,6 +270,11 @@ async def lnurlwallet(request: Request):
)
@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)
@@ -251,21 +282,23 @@ async def manifest(usr: str):
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return {
"short_name": "LNbits",
"name": "LNbits Wallet",
"short_name": LNBITS_SITE_TITLE,
"name": LNBITS_SITE_TITLE + " Wallet",
"icons": [
{
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"src": LNBITS_CUSTOM_LOGO
if 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,
"background_color": "#3367D6",
"description": "Weather forecast information",
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"theme_color": "#1F2234",
"shortcuts": [
{
"name": wallet.name,
+2 -1
View File
@@ -4,6 +4,7 @@ from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import HTTPException
from loguru import logger
from starlette.requests import Request
from starlette.responses import HTMLResponse
@@ -45,7 +46,7 @@ async def api_public_payment_longpolling(payment_hash):
payment_queue = asyncio.Queue(0)
print("adding standalone invoice listener", payment_hash, payment_queue)
logger.debug("adding standalone invoice listener", payment_hash, payment_queue)
api_invoice_listeners.append(payment_queue)
response = None
+11 -4
View File
@@ -5,6 +5,7 @@ import time
from contextlib import asynccontextmanager
from typing import Optional
from loguru import logger
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
@@ -130,10 +131,16 @@ class Database(Compat):
)
)
else:
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
if os.path.isdir(LNBITS_DATA_FOLDER):
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created"
f" - please 'mkdir {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:]
+45 -3
View File
@@ -13,7 +13,11 @@ 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.requestvars import g
from lnbits.settings import LNBITS_ALLOWED_USERS
from lnbits.settings import (
LNBITS_ADMIN_EXTENSIONS,
LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
)
class KeyChecker(SecurityBase):
@@ -122,6 +126,7 @@ async def get_key_type(
# 0: admin
# 1: invoice
# 2: invalid
pathname = r["path"].split("/")[1]
if not api_key_header and not api_key_query:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@@ -131,7 +136,14 @@ async def get_key_type(
try:
checker = WalletAdminKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(0, checker.wallet)
wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
):
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return wallet
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise
@@ -143,7 +155,14 @@ async def get_key_type(
try:
checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(1, checker.wallet)
wallet = WalletTypeInfo(1, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
):
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return wallet
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise
@@ -172,6 +191,26 @@ async def require_admin_key(
return wallet
async def require_invoice_key(
r: Request,
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
token = api_key_header if api_key_header else api_key_query
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
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
else:
return wallet
async def check_user_exists(usr: UUID4) -> User:
g().user = await get_user(usr.hex)
if not g().user:
@@ -184,4 +223,7 @@ async def check_user_exists(usr: UUID4) -> User:
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
if LNBITS_ADMIN_USERS and g().user.id in LNBITS_ADMIN_USERS:
g().user.admin = True
return g().user
+1 -1
View File
@@ -9,7 +9,7 @@ db = Database("ext_bleskomat")
bleskomat_static_files = [
{
"path": "/bleskomat/static",
"app": StaticFiles(directory="lnbits/extensions/bleskomat/static"),
"app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
"name": "bleskomat_static",
}
]
@@ -1,7 +1,8 @@
import httpx
import json
import os
import httpx
fiat_currencies = json.load(
open(
os.path.join(
+2 -1
View File
@@ -3,6 +3,7 @@ import math
import traceback
from http import HTTPStatus
from loguru import logger
from starlette.requests import Request
from . import bleskomat_ext
@@ -122,7 +123,7 @@ async def api_bleskomat_lnurl(req: Request):
except LnurlHttpError as e:
return {"status": "ERROR", "reason": str(e)}
except Exception as e:
print(str(e))
logger.error(str(e))
return {"status": "ERROR", "reason": "Unexpected error"}
return {"status": "OK"}
+3 -2
View File
@@ -3,11 +3,12 @@ import time
from typing import Dict
from fastapi.params import Query
from loguru import logger
from pydantic import BaseModel, validator
from starlette.requests import Request
from lnbits import bolt11
from lnbits.core.services import pay_invoice, PaymentFailure
from lnbits.core.services import PaymentFailure, pay_invoice
from . import db
from .exchange_rates import exchange_rate_providers, fiat_currencies
@@ -125,7 +126,7 @@ class BleskomatLnurl(BaseModel):
except (ValueError, PermissionError, PaymentFailure) as e:
raise LnurlValidationError("Failed to pay invoice: " + str(e))
except Exception as e:
print(str(e))
logger.error(str(e))
raise LnurlValidationError("Unexpected error")
async def use(self, conn) -> bool:
@@ -62,4 +62,5 @@
</p>
</q-card-section>
</q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/Bleskomat"></q-btn>
</q-expansion-item>
+2 -1
View File
@@ -1,6 +1,7 @@
from http import HTTPStatus
from fastapi import Depends, Query
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
@@ -60,7 +61,7 @@ async def api_bleskomat_create_or_update(
currency=fiat_currency, provider=exchange_rate_provider
)
except Exception as e:
print(e)
logger.error(e)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"',
+1 -1
View File
@@ -12,7 +12,7 @@ db = Database("ext_copilot")
copilot_static_files = [
{
"path": "/copilot/static",
"app": StaticFiles(directory="lnbits/extensions/copilot/static"),
"app": StaticFiles(packages=[("lnbits", "extensions/copilot/static")]),
"name": "copilot_static",
}
]
+4 -3
View File
@@ -1,8 +1,9 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Copilots, CreateCopilotData
from lnbits.helpers import urlsafe_short_hash
###############COPILOTS##########################
@@ -15,7 +16,7 @@ async def create_copilot(
"""
INSERT INTO copilot.newer_copilots (
id,
user,
"user",
lnurl_toggle,
wallet,
title,
@@ -87,7 +88,7 @@ async def get_copilot(copilot_id: str) -> Copilots:
async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall(
"SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,)
'SELECT * FROM copilot.newer_copilots WHERE "user" = ?', (user,)
)
return [Copilots(**row) for row in rows]
+9 -7
View File
@@ -1,12 +1,14 @@
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
import json
from sqlite3 import Row
from typing import Dict, Optional
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
from starlette.requests import Request
from lnbits.lnurl import encode as lnurl_encode # type: ignore
class CreateCopilotData(BaseModel):
+1 -1
View File
@@ -25,7 +25,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
webhook = None
data = None
if "copilot" != payment.extra.get("tag"):
if payment.extra.get("tag") != "copilot":
# not an copilot invoice
return
@@ -14,6 +14,7 @@
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/copilot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="Create copilot">
<q-card>
<q-card-section>
@@ -31,8 +32,8 @@
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/copilot -d '{"title":
&lt;string&gt;, "animation": &lt;string&gt;,
>curl -X POST {{ request.base_url }}copilot/api/v1/copilot -d
'{"title": &lt;string&gt;, "animation": &lt;string&gt;,
"show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"lnurl_title": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{user.wallets[0].adminkey }}"
@@ -59,11 +60,11 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -d '{"title": &lt;string&gt;,
"animation": &lt;string&gt;, "show_message":&lt;string&gt;,
"amount": &lt;integer&gt;, "lnurl_title": &lt;string&gt;}' -H
"Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
}}copilot/api/v1/copilot/&lt;copilot_id&gt; -d '{"title":
&lt;string&gt;, "animation": &lt;string&gt;,
"show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"lnurl_title": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@@ -87,8 +88,9 @@
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/copilot/&lt;copilot_id&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
>curl -X GET {{ request.base_url
}}copilot/api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
@@ -110,8 +112,8 @@
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/copilots -H "X-Api-Key: {{
user.wallets[0].inkey }}"
>curl -X GET {{ request.base_url }}copilot/api/v1/copilots -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
@@ -136,7 +138,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
}}copilot/api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
@@ -161,9 +163,10 @@
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}/api/v1/copilot/ws/&lt;string,
copilot_id&gt;/&lt;string, comment&gt;/&lt;string, gif name&gt; -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
>curl -X GET {{ request.base_url
}}copilot/api/v1/copilot/ws/&lt;string, copilot_id&gt;/&lt;string,
comment&gt;/&lt;string, gif name&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
+11
View File
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
[requires]
python_version = "3.9"
+34
View File
@@ -0,0 +1,34 @@
# Discord Bot
## Provide LNbits wallets for all your Discord users
_This extension is a modifed version of LNbits [User Manager](../usermanager/README.md)_
The intended usage of this extension is to connect it to a specifically designed [Discord Bot](https://github.com/chrislennon/lnbits-discord-bot) leveraging LNbits as a community based lightning node.
## Setup
This bot can target [lnbits.com](https://lnbits.com) or a self hosted instance.
To setup and run the bot instructions are located [here](https://github.com/chrislennon/lnbits-discord-bot#installation)
## Usage
This bot will allow users to interact with it in the following ways [full command list](https://github.com/chrislennon/lnbits-discord-bot#commands):
`/create` Will create a wallet for the Discord user
- (currently limiting 1 Discord user == 1 LNbits user == 1 user wallet)
![create](https://imgur.com/CWdDusE.png)
`/balance` Will show the balance of the users wallet.
![balance](https://imgur.com/tKeReCp.png)
`/tip @user [amount]` Will sent money from one user to another
- If the recieving user does not have a wallet, one will be created for them
- The receiving user will receive a direct message from the bot with a link to their wallet
![tip](https://imgur.com/K3tnChK.png)
`/payme [amount] [description]` Will open an invoice that can be paid by any user
![payme](https://imgur.com/dFvAqL3.png)
+25
View File
@@ -0,0 +1,25 @@
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_discordbot")
discordbot_static_files = [
{
"path": "/discordbot/static",
"app": StaticFiles(packages=[("lnbits", "extensions/discordbot/static")]),
"name": "discordbot_static",
}
]
discordbot_ext: APIRouter = APIRouter(prefix="/discordbot", tags=["discordbot"])
def discordbot_renderer():
return template_renderer(["lnbits/extensions/discordbot/templates"])
from .views import * # noqa
from .views_api import * # noqa
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Discord Bot",
"short_description": "Generate users and wallets",
"icon": "person_add",
"contributors": ["bitcoingamer21"]
}
+123
View File
@@ -0,0 +1,123 @@
from typing import List, Optional
from lnbits.core.crud import (
create_account,
create_wallet,
delete_wallet,
get_payments,
get_user,
)
from lnbits.core.models import Payment
from . import db
from .models import CreateUserData, Users, Wallets
### Users
async def create_discordbot_user(data: CreateUserData) -> Users:
account = await create_account()
user = await get_user(account.id)
assert user, "Newly created user couldn't be retrieved"
wallet = await create_wallet(user_id=user.id, wallet_name=data.wallet_name)
await db.execute(
"""
INSERT INTO discordbot.users (id, name, admin, discord_id)
VALUES (?, ?, ?, ?)
""",
(user.id, data.user_name, data.admin_id, data.discord_id),
)
await db.execute(
"""
INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
wallet.id,
data.admin_id,
data.wallet_name,
user.id,
wallet.adminkey,
wallet.inkey,
),
)
user_created = await get_discordbot_user(user.id)
assert user_created, "Newly created user couldn't be retrieved"
return user_created
async def get_discordbot_user(user_id: str) -> Optional[Users]:
row = await db.fetchone("SELECT * FROM discordbot.users WHERE id = ?", (user_id,))
return Users(**row) if row else None
async def get_discordbot_users(user_id: str) -> List[Users]:
rows = await db.fetchall(
"SELECT * FROM discordbot.users WHERE admin = ?", (user_id,)
)
return [Users(**row) for row in rows]
async def delete_discordbot_user(user_id: str) -> None:
wallets = await get_discordbot_wallets(user_id)
for wallet in wallets:
await delete_wallet(user_id=user_id, wallet_id=wallet.id)
await db.execute("DELETE FROM discordbot.users WHERE id = ?", (user_id,))
await db.execute("""DELETE FROM discordbot.wallets WHERE "user" = ?""", (user_id,))
### Wallets
async def create_discordbot_wallet(
user_id: str, wallet_name: str, admin_id: str
) -> Wallets:
wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
await db.execute(
"""
INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?, ?)
""",
(wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey),
)
wallet_created = await get_discordbot_wallet(wallet.id)
assert wallet_created, "Newly created wallet couldn't be retrieved"
return wallet_created
async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
row = await db.fetchone(
"SELECT * FROM discordbot.wallets WHERE id = ?", (wallet_id,)
)
return Wallets(**row) if row else None
async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
rows = await db.fetchall(
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
)
return [Wallets(**row) for row in rows]
async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
rows = await db.fetchall(
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
)
return [Wallets(**row) for row in rows]
async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
return await get_payments(
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
)
async def delete_discordbot_wallet(wallet_id: str, user_id: str) -> None:
await delete_wallet(user_id=user_id, wallet_id=wallet_id)
await db.execute("DELETE FROM discordbot.wallets WHERE id = ?", (wallet_id,))
@@ -0,0 +1,30 @@
async def m001_initial(db):
"""
Initial users table.
"""
await db.execute(
"""
CREATE TABLE discordbot.users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
admin TEXT NOT NULL,
discord_id TEXT
);
"""
)
"""
Initial wallets table.
"""
await db.execute(
"""
CREATE TABLE discordbot.wallets (
id TEXT PRIMARY KEY,
admin TEXT NOT NULL,
name TEXT NOT NULL,
"user" TEXT NOT NULL,
adminkey TEXT NOT NULL,
inkey TEXT NOT NULL
);
"""
)
+38
View File
@@ -0,0 +1,38 @@
from sqlite3 import Row
from typing import Optional
from fastapi.param_functions import Query
from pydantic import BaseModel
class CreateUserData(BaseModel):
user_name: str = Query(...)
wallet_name: str = Query(...)
admin_id: str = Query(...)
discord_id: str = Query("")
class CreateUserWallet(BaseModel):
user_id: str = Query(...)
wallet_name: str = Query(...)
admin_id: str = Query(...)
class Users(BaseModel):
id: str
name: str
admin: str
discord_id: str
class Wallets(BaseModel):
id: str
admin: str
name: str
user: str
adminkey: str
inkey: str
@classmethod
def from_row(cls, row: Row) -> "Wallets":
return cls(**dict(row))
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

@@ -0,0 +1,269 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Discord Bot: Connect Discord users to LNbits.
</h5>
<p>
Connect your LNbits instance to a
<a href="https://github.com/chrislennon/lnbits-discord-bot"
>Discord Bot</a
>
leveraging LNbits as a community based lightning node.<br />
<small>
Created by,
<a href="https://github.com/chrislennon">Chris Lennon</a></small
>
<br />
<small>
Based on User Manager, by
<a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/discordbot"></q-btn>
<q-expansion-item group="api" dense expand-separator label="GET users">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/discordbot/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}discordbot/api/v1/users -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET user">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/discordbot/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}discordbot/api/v1/users/&lt;user_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET wallets">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/discordbot/api/v1/wallets/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON wallet data</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}discordbot/api/v1/wallets/&lt;user_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET transactions">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/discordbot/api/v1/wallets&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON a wallets transactions</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}discordbot/api/v1/wallets&lt;wallet_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST user + initial wallet"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/discordbot/api/v1/users</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": &lt;string&gt;, "Content-type":
"application/json"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;,"discord_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "name": &lt;string&gt;, "admin":
&lt;string&gt;, "discord_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}discordbot/api/v1/users -d
'{"admin_id": "{{ user.id }}", "wallet_name": &lt;string&gt;,
"user_name": &lt;string&gt;, "discord_id": &lt;string&gt;}' -H
"X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="POST wallet">
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/discordbot/api/v1/wallets</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": &lt;string&gt;, "Content-type":
"application/json"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json) - "admin_id" is a YOUR user ID
</h5>
<code
>{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "admin": &lt;string&gt;, "name":
&lt;string&gt;, "user": &lt;string&gt;, "adminkey": &lt;string&gt;,
"inkey": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}discordbot/api/v1/wallets -d
'{"user_id": &lt;string&gt;, "wallet_name": &lt;string&gt;,
"admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey
}}" -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="DELETE user and their wallets"
>
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/discordbot/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}discordbot/api/v1/users/&lt;user_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="DELETE wallet">
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/discordbot/api/v1/wallets/&lt;wallet_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}discordbot/api/v1/wallets/&lt;wallet_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="POST activate extension"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/discordbot/api/v1/extensions</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}discordbot/api/v1/extensions -d
'{"userid": &lt;string&gt;, "extension": &lt;string&gt;, "active":
&lt;integer&gt;}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H
"Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -0,0 +1,470 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<center><img src="/discordbot/static/stack.png" height="200" /></center>
This extension is designed to be used through its API by a Discord Bot,
currently you have to install the bot
<a
href="https://github.com/chrislennon/lnbits-discord-bot/#installation"
>yourself</a
><br />
Soon™ there will be a much easier one-click install discord bot...
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Users</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportUsersCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="users"
row-key="id"
:columns="usersTable.columns"
:pagination.sync="usersTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteUser(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Wallets</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportWalletsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="wallets"
row-key="id"
:columns="walletsTable.columns"
:pagination.sync="walletsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="account_balance_wallet"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.walllink"
target="_blank"
></q-btn>
<q-tooltip> Link to wallet </q-tooltip>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteWallet(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits Discord Bot Extension
<!--{{SITE_TITLE}} Discord Bot Extension-->
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "discordbot/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="userDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendUserFormData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="userDialog.data.usrname"
label="Username"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.walname"
label="Initial wallet name"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.discord_id"
label="Discord ID"
></q-input>
<q-btn
unelevated
color="primary"
:disable="userDialog.data.walname == null"
type="submit"
>Create User</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="walletDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendWalletFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="walletDialog.data.user"
:options="userOptions"
label="User *"
>
</q-select>
<q-input
filled
dense
v-model.trim="walletDialog.data.walname"
label="Wallet name"
></q-input>
<q-btn
unelevated
color="primary"
:disable="walletDialog.data.walname == null"
type="submit"
>Create Wallet</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapUserManager = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.walllink = ['../wallet?usr=', obj.user, '&wal=', obj.id].join('')
obj._data = _.clone(obj)
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
wallets: [],
users: [],
usersTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'},
{
name: 'discord_id',
align: 'left',
label: 'discord_id',
field: 'discord_id'
}
],
pagination: {
rowsPerPage: 10
}
},
walletsTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'user', align: 'left', label: 'User', field: 'user'},
{
name: 'adminkey',
align: 'left',
label: 'Admin Key',
field: 'adminkey'
},
{name: 'inkey', align: 'left', label: 'Invoice Key', field: 'inkey'}
],
pagination: {
rowsPerPage: 10
}
},
walletDialog: {
show: false,
data: {}
},
userDialog: {
show: false,
data: {}
}
}
},
computed: {
userOptions: function () {
return this.users.map(function (obj) {
console.log(obj.id)
return {
value: String(obj.id),
label: String(obj.id)
}
})
}
},
methods: {
///////////////Users////////////////////////////
getUsers: function () {
var self = this
LNbits.api
.request(
'GET',
'/discordbot/api/v1/users',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.users = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openUserUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.userDialog.data = _.clone(link._data)
this.userDialog.show = true
},
sendUserFormData: function () {
if (this.userDialog.data.id) {
} else {
var data = {
admin_id: this.g.user.id,
user_name: this.userDialog.data.usrname,
wallet_name: this.userDialog.data.walname,
discord_id: this.userDialog.data.discord_id
}
}
{
this.createUser(data)
}
},
createUser: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/discordbot/api/v1/users',
this.g.user.wallets[0].inkey,
data
)
.then(function (response) {
self.users.push(mapUserManager(response.data))
self.userDialog.show = false
self.userDialog.data = {}
data = {}
self.getWallets()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteUser: function (userId) {
var self = this
console.log(userId)
LNbits.utils
.confirmDialog('Are you sure you want to delete this User link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/discordbot/api/v1/users/' + userId,
self.g.user.wallets[0].inkey
)
.then(function (response) {
self.users = _.reject(self.users, function (obj) {
return obj.id == userId
})
self.getWallets()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportUsersCSV: function () {
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
},
///////////////Wallets////////////////////////////
getWallets: function () {
var self = this
LNbits.api
.request(
'GET',
'/discordbot/api/v1/wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.wallets = response.data.map(function (obj) {
return mapUserManager(obj)
})
})
},
openWalletUpdateDialog: function (linkId) {
var link = _.findWhere(this.users, {id: linkId})
this.walletDialog.data = _.clone(link._data)
this.walletDialog.show = true
},
sendWalletFormData: function () {
if (this.walletDialog.data.id) {
} else {
var data = {
user_id: this.walletDialog.data.user,
admin_id: this.g.user.id,
wallet_name: this.walletDialog.data.walname
}
}
{
this.createWallet(data)
}
},
createWallet: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/discordbot/api/v1/wallets',
this.g.user.wallets[0].inkey,
data
)
.then(function (response) {
self.wallets.push(mapUserManager(response.data))
self.walletDialog.show = false
self.walletDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteWallet: function (userId) {
var self = this
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/discordbot/api/v1/wallets/' + userId,
self.g.user.wallets[0].inkey
)
.then(function (response) {
self.wallets = _.reject(self.wallets, function (obj) {
return obj.id == userId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportWalletsCSV: function () {
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getUsers()
this.getWallets()
}
}
})
</script>
{% endblock %}
+15
View File
@@ -0,0 +1,15 @@
from fastapi import Request
from fastapi.params import Depends
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import discordbot_ext, discordbot_renderer
@discordbot_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return discordbot_renderer().TemplateResponse(
"discordbot/index.html", {"request": request, "user": user.dict()}
)
+125
View File
@@ -0,0 +1,125 @@
from http import HTTPStatus
from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core import update_user_extension
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from . import discordbot_ext
from .crud import (
create_discordbot_user,
create_discordbot_wallet,
delete_discordbot_user,
delete_discordbot_wallet,
get_discordbot_user,
get_discordbot_users,
get_discordbot_users_wallets,
get_discordbot_wallet,
get_discordbot_wallet_transactions,
get_discordbot_wallets,
)
from .models import CreateUserData, CreateUserWallet
# Users
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
user_id = wallet.wallet.user
return [user.dict() for user in await get_discordbot_users(user_id)]
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
user = await get_discordbot_user(user_id)
return user.dict()
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
async def api_discordbot_users_create(
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await create_discordbot_user(data)
full = user.dict()
full["wallets"] = [
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
]
return full
@discordbot_ext.delete("/api/v1/users/{user_id}")
async def api_discordbot_users_delete(
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await get_discordbot_user(user_id)
if not user:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
)
await delete_discordbot_user(user_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
# Activate Extension
@discordbot_ext.post("/api/v1/extensions")
async def api_discordbot_activate_extension(
extension: str = Query(...), userid: str = Query(...), active: bool = Query(...)
):
user = await get_user(userid)
if not user:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
)
update_user_extension(user_id=userid, extension=extension, active=active)
return {"extension": "updated"}
# Wallets
@discordbot_ext.post("/api/v1/wallets")
async def api_discordbot_wallets_create(
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await create_discordbot_wallet(
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
)
return user.dict()
@discordbot_ext.get("/api/v1/wallets")
async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
admin_id = wallet.wallet.user
return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
async def api_discordbot_wallet_transactions(
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
return await get_discordbot_wallet_transactions(wallet_id)
@discordbot_ext.get("/api/v1/wallets/{user_id}")
async def api_discordbot_users_wallets(
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
async def api_discordbot_wallets_delete(
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
):
get_wallet = await get_discordbot_wallet(wallet_id)
if not get_wallet:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist."
)
await delete_discordbot_wallet(wallet_id, get_wallet.user)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+2 -2
View File
@@ -16,7 +16,7 @@ async def create_ticket(
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(payment_hash, wallet, event, name, email, False, False),
(payment_hash, wallet, event, name, email, False, True),
)
ticket = await get_ticket(payment_hash)
@@ -76,7 +76,7 @@ async def delete_ticket(payment_hash: str) -> None:
async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.tickets WHERE event = ?", (event_id,))
await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,))
# EVENTS
@@ -20,4 +20,5 @@
</p>
</q-card-section>
</q-card>
<q-btn flat label="Swagger API" type="a" href="../docs#/events"></q-btn>
</q-expansion-item>
@@ -135,15 +135,7 @@
var self = this
axios
.post(
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.get('/events/api/v1/tickets/' + '{{ event_id }}')
.then(function (response) {
self.paymentReq = response.data.payment_request
self.paymentCheck = response.data.payment_hash
@@ -161,7 +153,17 @@
paymentChecker = setInterval(function () {
axios
.get('/events/api/v1/tickets/' + self.paymentCheck)
.post(
'/events/api/v1/tickets/' +
'{{ event_id }}/' +
self.paymentCheck,
{
event: '{{ event_id }}',
event_name: '{{ event_name }}',
name: self.formDialog.data.name,
email: self.formDialog.data.email
}
)
.then(function (res) {
if (res.data.paid) {
clearInterval(paymentChecker)
@@ -381,10 +381,10 @@
getTickets: function () {
var self = this
LNbits.api
.request(
'GET',
'/events/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey
.request(
'GET',
'/events/api/v1/tickets?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
console.log(response)
@@ -133,7 +133,10 @@
var self = this
LNbits.api
.request('GET', '/events/api/v1/register/ticket/' + res)
.request(
'GET',
'/events/api/v1/register/ticket/' + res.split('//')[1]
)
.then(function (response) {
self.$q.notify({
type: 'positive',
@@ -13,9 +13,8 @@
<br />
<qrcode
:value="'{{ ticket_id }}'"
:options="{width: 340}"
class="rounded-borders"
:value="'ticket://{{ ticket_id }}'"
:options="{width: 500}"
></qrcode>
<br />
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
+19 -21
View File
@@ -97,8 +97,8 @@ async def api_tickets(
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@events_ext.post("/api/v1/tickets/{event_id}/{sats}")
async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
@events_ext.get("/api/v1/tickets/{event_id}")
async def api_ticket_make_ticket(event_id):
event = await get_event(event_id)
if not event:
raise HTTPException(
@@ -107,37 +107,35 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
try:
payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet,
amount=int(sats),
amount=event.price_per_ticket,
memo=f"{event_id}",
extra={"tag": "events"},
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
ticket = await create_ticket(
payment_hash=payment_hash,
wallet=event.wallet,
event=event_id,
name=data.name,
email=data.email,
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
)
return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.get("/api/v1/tickets/{payment_hash}")
async def api_ticket_send_ticket(payment_hash):
ticket = await get_ticket(payment_hash)
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
event = await get_event(event_id)
try:
status = await api_payment(payment_hash)
if status["paid"]:
await set_ticket_paid(payment_hash=payment_hash)
ticket = await create_ticket(
payment_hash=payment_hash,
wallet=event.wallet,
event=event_id,
name=data.name,
email=data.email,
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
)
return {"paid": True, "ticket_id": ticket.id}
except Exception:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")
+11
View File
@@ -0,0 +1,11 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
This is an example extension to help you organise and build you own.
Try to include an image
<img src="https://i.imgur.com/9i4xcQB.png">
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
+16
View File
@@ -0,0 +1,16 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_example")
example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"])
def example_renderer():
return template_renderer(["lnbits/extensions/example/templates"])
from .views import * # noqa
from .views_api import * # noqa
@@ -0,0 +1,6 @@
{
"name": "Build your own!!",
"short_description": "Join us, make an extension",
"icon": "info",
"contributors": ["github_username"]
}
+10
View File
@@ -0,0 +1,10 @@
# async def m001_initial(db):
# await db.execute(
# f"""
# CREATE TABLE example.example (
# id TEXT PRIMARY KEY,
# wallet TEXT NOT NULL,
# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
# );
# """
# )
+5
View File
@@ -0,0 +1,5 @@
# from pydantic import BaseModel
# class Example(BaseModel):
# id: str
# wallet: str
@@ -0,0 +1,59 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md">
Frameworks used by {{SITE_TITLE}}
</h5>
<q-list>
<q-item
v-for="tool in tools"
:key="tool.name"
tag="a"
:href="tool.url"
target="_blank"
>
{% raw %}
<!-- with raw Flask won't try to interpret the Vue moustaches -->
<q-item-section>
<q-item-label>{{ tool.name }}</q-item-label>
<q-item-label caption>{{ tool.language }}</q-item-label>
</q-item-section>
{% endraw %}
</q-item>
</q-list>
<q-separator class="q-my-lg"></q-separator>
<p>
A magical "g" is always available, with info about the user, wallets and
extensions:
</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-card-section>
</q-card>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tools: []
}
},
created: function () {
var self = this
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data
})
}
})
</script>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
from fastapi import FastAPI, Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import example_ext, example_renderer
templates = Jinja2Templates(directory="templates")
@example_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return example_renderer().TemplateResponse(
"example/index.html", {"request": request, "user": user.dict()}
)
+35
View File
@@ -0,0 +1,35 @@
# views_api.py is for you API endpoints that could be hit by another service
# add your dependencies here
# import httpx
# (use httpx just like requests, except instead of response.ok there's only the
# response.is_error that is its inverse)
from . import example_ext
# add your endpoints here
@example_ext.get("/api/v1/tools")
async def api_example():
"""Try to add descriptions for others."""
tools = [
{
"name": "fastAPI",
"url": "https://fastapi.tiangolo.com/",
"language": "Python",
},
{
"name": "Vue.js",
"url": "https://vuejs.org/",
"language": "JavaScript",
},
{
"name": "Quasar Framework",
"url": "https://quasar.dev/",
"language": "JavaScript",
},
]
return tools
+1 -1
View File
@@ -12,7 +12,7 @@ db = Database("ext_jukebox")
jukebox_static_files = [
{
"path": "/jukebox/static",
"app": StaticFiles(directory="lnbits/extensions/jukebox/static"),
"app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
"name": "jukebox_static",
}
]
+4 -3
View File
@@ -12,7 +12,7 @@ async def create_jukebox(
juke_id = urlsafe_short_hash()
result = await db.execute(
"""
INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
@@ -41,6 +41,7 @@ async def update_jukebox(
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
items.append(juke_id)
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None
@@ -57,11 +58,11 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
async def get_jukeboxs(user: str) -> List[Jukebox]:
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,))
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
for row in rows:
if row.sp_playlists == None:
await delete_jukebox(row.id)
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,))
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
return [Jukebox(**row) for row in rows]
+3 -3
View File
@@ -1,9 +1,9 @@
from typing import NamedTuple
from sqlite3 import Row
from typing import NamedTuple, Optional
from fastapi.param_functions import Query
from pydantic.main import BaseModel
from pydantic import BaseModel
from typing import Optional
from pydantic.main import BaseModel
class CreateJukeLinkData(BaseModel):
+7 -10
View File
@@ -3,9 +3,9 @@
Vue.component(VueQrcode.name, VueQrcode)
var mapJukebox = obj => {
if(obj.sp_device){
if (obj.sp_device) {
obj._data = _.clone(obj)
obj.sp_id = obj._data.id
obj.device = obj._data.sp_device.split('-')[0]
playlists = obj._data.sp_playlists.split(',')
@@ -17,11 +17,9 @@ var mapJukebox = obj => {
obj.playlist = playlistsar.join()
console.log(obj)
return obj
}
else {
} else {
return
}
}
new Vue({
@@ -87,14 +85,14 @@ new Vue({
var link = _.findWhere(this.JukeboxLinks, {id: linkId})
this.qrCodeDialog.data = _.clone(link)
this.qrCodeDialog.data.url =
window.location.protocol + '//' + window.location.host
this.qrCodeDialog.show = true
},
getJukeboxes() {
self = this
LNbits.api
.request(
'GET',
@@ -103,8 +101,7 @@ new Vue({
)
.then(function (response) {
self.JukeboxLinks = response.data.map(function (obj) {
return mapJukebox(obj)
return mapJukebox(obj)
})
console.log(self.JukeboxLinks)
})
@@ -154,7 +151,7 @@ new Vue({
submitSpotifyKeys() {
self = this
self.jukeboxDialog.data.user = self.g.user.id
LNbits.api
.request(
'POST',
+1 -1
View File
@@ -16,7 +16,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
if "jukebox" != payment.extra.get("tag"):
if payment.extra.get("tag") != "jukebox":
# not a jukebox invoice
return
await update_jukebox_payment(payment.payment_hash, paid=True)
@@ -24,6 +24,8 @@
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List jukeboxes">
<q-card>
<q-card-section>
@@ -37,8 +39,8 @@
<code>[&lt;jukebox_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/jukebox -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
>curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@@ -59,8 +61,9 @@
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/jukebox/&lt;juke_id&gt; -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
>curl -X GET {{ request.base_url
}}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@@ -86,8 +89,8 @@
<code>&lt;jukbox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/jukebox/ -d '{"user":
&lt;string, user_id&gt;, "title": &lt;string&gt;,
>curl -X POST {{ request.base_url }}jukebox/api/v1/jukebox/ -d
'{"user": &lt;string, user_id&gt;, "title": &lt;string&gt;,
"wallet":&lt;string&gt;, "sp_user": &lt;string,
spotify_user_account&gt;, "sp_secret": &lt;string,
spotify_user_secret&gt;, "sp_access_token": &lt;string,
@@ -116,8 +119,9 @@
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url }}api/v1/jukebox/&lt;juke_id&gt;
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
>curl -X DELETE {{ request.base_url
}}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@@ -117,7 +117,7 @@
>
<q-step
:name="1"
title="Pick wallet, price"
title="1. Pick Wallet and Price"
icon="account_balance_wallet"
:done="step > 1"
>
@@ -170,16 +170,25 @@
<br />
</q-step>
<q-step :name="2" title="Add api keys" icon="vpn_key" :done="step > 2">
<q-step
:name="2"
title="2. Add API keys"
icon="vpn_key"
:done="step > 2"
>
<img src="/jukebox/static/spotapi.gif" />
To use this extension you need a Spotify client ID and client secret.
You get these by creating an app in the Spotify developers dashboard
<a
You get these by creating an app in the Spotify Developer Dashboard
<br />
<br />
<q-btn
type="a"
target="_blank"
style="color: #43a047"
color="primary"
href="https://developer.spotify.com/dashboard/applications"
>here</a
>.
>Open the Spotify Developer Dashboard</q-btn
>
<q-input
filled
class="q-pb-md q-pt-md"
@@ -231,28 +240,39 @@
<br />
</q-step>
<q-step :name="3" title="Add Redirect URI" icon="link" :done="step > 3">
<q-step
:name="3"
title="3. Add Redirect URI"
icon="link"
:done="step > 3"
>
<img src="/jukebox/static/spotapi1.gif" />
In the app go to edit-settings, set the redirect URI to this link
<p>
In the app go to edit-settings, set the redirect URI to this link
</p>
<q-card
class="cursor-pointer word-break"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>
<q-card-section style="word-break: break-all">
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}
</q-card-section>
<q-tooltip> Click to copy URL </q-tooltip>
</q-card>
<br />
<q-btn
dense
outline
unelevated
color="primary"
size="xs"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<br />
Settings can be found
<a
type="a"
target="_blank"
style="color: #43a047"
color="primary"
href="https://developer.spotify.com/dashboard/applications"
>here</a
>.
>Open the Spotify Application Settings</q-btn
>
<br /><br />
<p>
After adding the redirect URI, click the "Authorise access" button
below.
</p>
<div class="row q-mt-md">
<div class="col-4">
@@ -281,7 +301,7 @@
<q-step
:name="4"
title="Select playlists"
title="4. Select Device and Playlists"
icon="queue_music"
active-color="primary"
:done="step > 4"
+3 -3
View File
@@ -75,7 +75,6 @@ async def api_check_credentials_check(
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
):
jukebox = await get_jukebox(juke_id)
return jukebox
@@ -442,7 +441,7 @@ async def api_get_jukebox_currently(
token = await api_get_token(juke_id)
if token == False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid"
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
)
elif retry:
raise HTTPException(
@@ -456,5 +455,6 @@ async def api_get_jukebox_currently(
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
status_code=HTTPStatus.NOT_FOUND,
detail="Something went wrong, or no song is playing yet",
)
+2 -2
View File
@@ -12,7 +12,7 @@ db = Database("ext_livestream")
livestream_static_files = [
{
"path": "/livestream/static",
"app": StaticFiles(directory="lnbits/extensions/livestream/static"),
"app": StaticFiles(packages=[("lnbits", "extensions/livestream/static")]),
"name": "livestream_static",
}
]
@@ -30,6 +30,6 @@ from .views import * # noqa
from .views_api import * # noqa
def lnticket_start():
def livestream_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+5 -3
View File
@@ -1,6 +1,8 @@
import asyncio
import json
from loguru import logger
from lnbits.core import db as core_db
from lnbits.core.crud import create_payment
from lnbits.core.models import Payment
@@ -20,17 +22,17 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None:
if "livestream" != payment.extra.get("tag"):
if payment.extra.get("tag") != "livestream":
# not a livestream invoice
return
track = await get_track(payment.extra.get("track", -1))
if not track:
print("this should never happen", payment)
logger.error("this should never happen", payment)
return
if payment.extra.get("shared_with"):
print("payment was shared already", payment)
logger.error("payment was shared already", payment)
return
producer = await get_producer(track.producer)
@@ -17,6 +17,8 @@
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/livestream"></q-btn>
<q-expansion-item
group="api"
dense
@@ -38,8 +40,8 @@
<code>[&lt;livestream_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{
user.wallets[0].inkey }}"
>curl -X GET {{ request.base_url }}livestream/api/v1/livestream -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
@@ -59,8 +61,8 @@
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root
}}api/v1/livestream/track/&lt;track_id&gt; -H "X-Api-Key: {{
>curl -X PUT {{ request.base_url }}
livestream/api/v1/livestream/track/&lt;track_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
@@ -81,8 +83,8 @@
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root
}}api/v1/livestream/fee/&lt;fee_pct&gt; -H "X-Api-Key: {{
>curl -X PUT {{ request.base_url }}
livestream/api/v1/livestream/fee/&lt;fee_pct&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
@@ -109,11 +111,12 @@
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d
'{"name": &lt;string&gt;, "download_url": &lt;string&gt;,
"price_msat": &lt;integer&gt;, "producer_id": &lt;integer&gt;,
"producer_name": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
>curl -X POST {{ request.base_url }}
livestream/api/v1/livestream/tracks -d '{"name": &lt;string&gt;,
"download_url": &lt;string&gt;, "price_msat": &lt;integer&gt;,
"producer_id": &lt;integer&gt;, "producer_name": &lt;string&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
@@ -123,6 +126,7 @@
dense
expand-separator
label="Delete a withdraw link"
class="q-pb-md"
>
<q-card>
<q-card-section>
@@ -136,8 +140,8 @@
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}api/v1/livestream/tracks/&lt;track_id&gt; -H "X-Api-Key: {{
>curl -X DELETE {{ request.base_url }}
livestream/api/v1/livestream/tracks/&lt;track_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
+2 -1
View File
@@ -1,5 +1,4 @@
from http import HTTPStatus
from mmap import MAP_DENYWRITE
from fastapi.param_functions import Depends
from fastapi.params import Query
@@ -14,6 +13,8 @@ from lnbits.decorators import check_user_exists
from . import livestream_ext, livestream_renderer
from .crud import get_livestream_by_track, get_track
# from mmap import MAP_DENYWRITE
@livestream_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
+5 -3
View File
@@ -1,6 +1,8 @@
from datetime import datetime, timedelta
from typing import List, Optional, Union
from loguru import logger
from lnbits.helpers import urlsafe_short_hash
from . import db
@@ -160,7 +162,7 @@ async def set_address_renewed(address_id: str, duration: int):
async def check_address_available(username: str, domain: str):
row, = await db.fetchone(
(row,) = await db.fetchone(
"SELECT COUNT(username) FROM lnaddress.address WHERE username = ? AND domain = ?",
(username, domain),
)
@@ -186,9 +188,9 @@ async def purge_addresses(domain_id: str):
) # give user 1 day to topup is address
if not paid and pay_expire:
print("DELETE UNP_PAY_EXP", r["username"])
logger.debug("DELETE UNP_PAY_EXP", r["username"])
await delete_address(r["id"])
if paid and expired:
print("DELETE PAID_EXP", r["username"])
logger.debug("DELETE PAID_EXP", r["username"])
await delete_address(r["id"])

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