Compare commits

...
219 Commits
Author SHA1 Message Date
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
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
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
115 changed files with 3333 additions and 1792 deletions
+19 -13
View File
@@ -5,10 +5,18 @@ QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
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 +25,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"
@@ -27,10 +33,10 @@ 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"
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
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 +50,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 +74,7 @@ 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
+1
View File
@@ -9,4 +9,5 @@ jobs:
- 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" ./
+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
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
+2
View File
@@ -29,6 +29,8 @@ uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*"
jinja2 = "3.0.1"
pyngrok = "*"
secp256k1 = "*"
pycryptodomex = "*"
[dev-packages]
black = "==20.8b1"
Generated
+553 -323
View File
File diff suppressed because it is too large Load Diff
+3 -8
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,6 +11,8 @@ LNbits
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
Use [lnbits.com](https://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:
@@ -34,11 +33,7 @@ LNbits is inspired by all the great work of [opennode.com](https://www.opennode.
## Running LNbits
See the [install guide](docs/guide/installation.md) for details on installation and setup.
### 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.
See the [install guide](docs/devs/installation.md) for details on installation and setup.
## LNbits as an account system
+15 -11
View File
@@ -1,12 +1,12 @@
## Python script to migrate an LNbits SQLite DB to Postgres
## All credits to @Fritz446 for the awesome work
## pip install psycopg2 OR psycopg2-binary
import os
import sqlite3
import psycopg2
import sqlite3
import os
# Python script to migrate an LNbits SQLite DB to Postgres
# All credits to @Fritz446 for the awesome work
# pip install psycopg2 OR psycopg2-binary
# Change these values as needed
@@ -34,7 +34,8 @@ def get_postgres_cursor():
def check_db_versions(sqdb):
sqlite = get_sqlite_cursor(sqdb)
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
del dblite["lnurlpos"] # wrongly added?
if "lnurlpos" in dblite:
del dblite["lnurlpos"]
sqlite.close()
postgres = get_postgres_cursor()
@@ -42,7 +43,7 @@ def check_db_versions(sqdb):
dbpost = dict(postgres.fetchall())
for key in dblite.keys():
if dblite[key] != dbpost[key]:
if key in dblite and key in dbpost and dblite[key] != dbpost[key]:
raise Exception(
f"sqlite database version ({dblite[key]}) of {key} doesn't match postgres database version {dbpost[key]}"
)
@@ -76,7 +77,10 @@ def insert_to_pg(query, data):
connection = cursor.connection
for d in data:
cursor.execute(query, d)
try:
cursor.execute(query, d)
except:
raise ValueError(f"Failed to insert {d}")
connection.commit()
cursor.close()
+28 -41
View File
@@ -7,59 +7,46 @@ nav_order: 1
# Installation
Download the latest stable release https://github.com/lnbits/lnbits/releases
LNbits uses [Pipenv][pipenv] to manage Python packages.
## Application dependencies
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
sudo apt-get install pipenv
pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev
# pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
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 any of the modules fails to install, try checking and upgrading your setupTool module.
`pip install -U setuptools`
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
```
## Running the server
If you wish to use a version of Python higher than 3.7:
Create the data folder and edit the .env file:
pipenv --python 3.8 install --dev
mkdir data
cp .env.example .env
sudo nano .env
You will need to copy `.env.example` to `.env`, then set variables there.
To then run the server for development purposes (includes hot-reload), use:
![Files](https://i.imgur.com/ri2zOe8.png)
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0 --reload
For production, use:
pipenv run python -m uvicorn lnbits.__main__:app --host 0.0.0.0
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
**Notes**:
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 reccomend 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.
+37 -3
View File
@@ -6,6 +6,7 @@ nav_order: 2
# Basic installation
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/
@@ -25,14 +26,13 @@ Download this repo and install the dependencies:
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits/
cd lnbits-legend/
# 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
@@ -50,6 +50,7 @@ You might also need to install additional packages or perform additional setup s
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
@@ -67,6 +68,39 @@ Hopefully, everything works and get migrated... Launch LNbits again and check if
# Additional guides
### 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
#Wants=lnd.service # you can uncomment these lines if you know what you're doing
#After=lnd.service # it will make sure that lnbits starts after lnd (replace with your own backend service)
[Service]
WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation
ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here
User=bitcoin # replace with the user that you're running lnbits on
Restart=always
TimeoutSec=120
RestartSec=30
Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time
[Install]
WantedBy=multi-user.target
```
Save the file and run the following commands:
```sh
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
### 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 -2
View File
@@ -17,6 +17,7 @@ A backend wallet can be configured using the following LNbits environment variab
### CLightning
Using this wallet requires the installation of the `pylightning` Python package.
If you want to use LNURLp you should use SparkWallet because of an issue with description_hash and CLightning.
- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet**
- `CLIGHTNING_RPC`: /file/path/lightning-rpc
@@ -35,16 +36,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_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
+12 -15
View File
@@ -1,6 +1,5 @@
import asyncio
import importlib
from lnbits.core.tasks import register_task_listeners
import sys
import traceback
import warnings
@@ -12,6 +11,7 @@ from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
import lnbits.settings
from lnbits.core.tasks import register_task_listeners
from .commands import db_migrate, handle_assets
from .core import core_app
@@ -45,14 +45,10 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
"/core/static", StaticFiles(directory="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
@@ -88,18 +84,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:
while True:
error_message, balance = await WALLET.status()
if not error_message:
break
warnings.warn(
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."
)
print("Retrying connection to backend in 5 seconds...")
await asyncio.sleep(5)
print(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
)
def register_routes(app: FastAPI) -> None:
+205 -4
View File
@@ -2,10 +2,14 @@ import bitstring # type: ignore
import re
import hashlib
from typing import List, NamedTuple, Optional
from bech32 import bech32_decode, CHARSET # type: ignore
from bech32 import bech32_encode, bech32_decode, CHARSET
from ecdsa import SECP256k1, VerifyingKey # type: ignore
from ecdsa.util import sigdecode_string # type: ignore
from binascii import unhexlify
import time
from decimal import Decimal
import embit
import secp256k1
class Route(NamedTuple):
@@ -116,6 +120,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:
@@ -146,6 +310,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()
@@ -156,9 +348,9 @@ def _trim_to_bytes(barr):
def _readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
outputindex=(short_channel_id & 0xFFFF),
blockheight=((short_channel_id >> 40) & 0xffffff),
transactionindex=((short_channel_id >> 16) & 0xffffff),
outputindex=(short_channel_id & 0xffff),
)
@@ -167,3 +359,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
+19 -4
View File
@@ -6,12 +6,11 @@ from urllib.parse import urlparse
from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db
from .models import User, Wallet, Payment, BalanceCheck
# 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,
)
@@ -217,6 +219,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 +265,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 +284,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"""
+1
View File
@@ -57,6 +57,7 @@ class User(BaseModel):
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
admin: bool = False
@property
def wallet_ids(self) -> List[str]:
+16 -13
View File
@@ -85,20 +85,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",
@@ -136,7 +133,7 @@ async def pay_invoice(
# 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,6 +142,10 @@ async def pay_invoice(
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
if wallet.balance_msat < 0:
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:
@@ -163,7 +164,9 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id)
else:
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat
)
if payment.checking_id:
async with db.connect() as conn:
await create_payment(
@@ -278,12 +281,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,8 +325,7 @@ 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}")
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete()
elif not status.pending:
print(
@@ -333,5 +335,6 @@ async def check_invoice_status(
return status
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int) -> int:
return max(1000, int(amount_msat * 0.01))
return max(2000, int(amount_msat * 0.01))
+35 -6
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]
+1 -1
View File
@@ -50,7 +50,7 @@ async def dispatch_invoice_listener(payment: Payment):
async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment._asdict()
data = payment.dict()
try:
r = await client.post(payment.webhook, json=data, timeout=40)
await mark_webhook_sent(payment, r.status_code)
+1 -1
View File
@@ -61,7 +61,7 @@
<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
&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>
+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>
+497 -374
View File
@@ -15,10 +15,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 +185,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 +273,470 @@
</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>
{% 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 %}
</div>
+110 -31
View File
@@ -3,15 +3,16 @@ 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 Query, Request, Header
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
from pydantic import BaseModel
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
from lnbits import bolt11, lnurl
@@ -22,16 +23,26 @@ from lnbits.decorators import (
WalletInvoiceKeyChecker,
WalletTypeInfo,
get_key_type,
require_admin_key
)
from lnbits.helpers import url_for
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
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 .. 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,6 +68,35 @@ 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())
@@ -72,7 +112,9 @@ 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)
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
)
for payment in pendingPayments:
await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
@@ -82,8 +124,8 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
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
@@ -99,9 +141,9 @@ 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:
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
@@ -126,8 +168,8 @@ 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:
save_balance_check(wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
@@ -138,7 +180,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,
@@ -193,8 +235,7 @@ async def api_payments_create(
invoiceData: CreateInvoiceData = Body(...),
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
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:
@@ -249,14 +290,14 @@ async def api_payments_pay_lnurl(
if invoice.amount_msat != data.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected {data['amount']} msat, got {invoice.amount_msat}.",
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}.",
)
# 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}.",
# )
extra = {}
@@ -323,7 +364,13 @@ async def api_payments_sse(
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash):
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
wallet = None
try:
if X_Api_Key.extra:
print("No key")
except:
wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment(payment_hash)
await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash)
@@ -332,13 +379,19 @@ async def api_payment(payment_hash):
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}
@@ -374,16 +427,14 @@ async def api_lnurlscan(code: str):
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())
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 +464,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 +513,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,
@@ -501,3 +556,27 @@ async def api_perform_lnurlauth(callback: str):
@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
+10 -5
View File
@@ -14,7 +14,12 @@ 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_SITE_TITLE,
SERVICE_FEE,
)
from ..crud import (
create_account,
@@ -43,9 +48,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,
@@ -115,6 +118,8 @@ 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]
@@ -221,7 +226,7 @@ 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)
+9 -3
View File
@@ -130,9 +130,15 @@ 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"
)
self.schema = self.name
if self.name.startswith("ext_"):
+33 -3
View File
@@ -13,7 +13,7 @@ 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_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
class KeyChecker(SecurityBase):
@@ -122,6 +122,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 +132,10 @@ 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 +147,10 @@ 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 +179,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 +211,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
+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]
+1 -1
View File
@@ -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
+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]
+2 -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,5 @@ 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"
)
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus
from mmap import MAP_DENYWRITE
# from mmap import MAP_DENYWRITE
from fastapi.param_functions import Depends
from fastapi.params import Query
+1 -1
View File
@@ -160,7 +160,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),
)
+4 -2
View File
@@ -52,7 +52,7 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
amount_received = amount
domain = await get_domain(address.domain)
base_url = (
address.wallet_endpoint[:-1]
if address.wallet_endpoint.endswith("/")
@@ -71,7 +71,9 @@ async def lnurl_callback(address_id, amount: int = Query(...)):
"out": False,
"amount": int(amount_received / 1000),
"description_hash": hashlib.sha256(
(await address.lnurlpay_metadata(domain=domain.domain)).encode("utf-8")
(await address.lnurlpay_metadata(domain=domain.domain)).encode(
"utf-8"
)
).hexdigest(),
"extra": {"tag": f"Payment to {address.username}@{domain.domain}"},
},
+1 -1
View File
@@ -53,5 +53,5 @@ class Addresses(BaseModel):
text = f"Payment to {self.username}"
identifier = f"{self.username}@{domain}"
metadata = [["text/plain", text], ["text/identifier", identifier]]
return LnurlPayMetadata(json.dumps(metadata))
+2 -2
View File
@@ -36,7 +36,7 @@ async def display(domain_id, request: Request):
wallet = await get_wallet(domain.wallet)
url = urlparse(str(request.url))
return lnaddress_renderer().TemplateResponse(
"lnaddress/display.html",
{
@@ -45,6 +45,6 @@ async def display(domain_id, request: Request):
"domain_domain": domain.domain,
"domain_cost": domain.cost,
"domain_wallet_inkey": wallet.inkey,
"root_url": f"{url.scheme}://{url.netloc}"
"root_url": f"{url.scheme}://{url.netloc}",
},
)
+26 -9
View File
@@ -1,4 +1,6 @@
import time
import asyncio
from base64 import urlsafe_b64encode
from http import HTTPStatus
@@ -11,7 +13,7 @@ from lnbits import bolt11
from lnbits.core.crud import delete_expired_invoices, get_payments
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.decorators import WalletTypeInfo
from lnbits.settings import WALLET
from lnbits.settings import WALLET, LNBITS_SITE_TITLE
from . import lndhub_ext
from .decorators import check_wallet, require_admin_key
@@ -55,14 +57,13 @@ async def lndhub_addinvoice(
_, pr = await create_invoice(
wallet_id=wallet.wallet.id,
amount=int(data.amt),
memo=data.memo or "received sats",
memo=data.memo or LNBITS_SITE_TITLE,
extra={"tag": "lndhub"},
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
)
invoice = bolt11.decode(pr)
return {
"pay_req": pr,
@@ -108,13 +109,17 @@ async def lndhub_payinvoice(
@lndhub_ext.get("/ext/balance")
async def lndhub_balance(wallet: WalletTypeInfo = Depends(check_wallet),):
async def lndhub_balance(
wallet: WalletTypeInfo = Depends(check_wallet),
):
return {"BTC": {"AvailableBalance": wallet.wallet.balance}}
@lndhub_ext.get("/ext/gettxs")
async def lndhub_gettxs(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200)
wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
):
for payment in await get_payments(
wallet_id=wallet.wallet.id,
@@ -122,11 +127,15 @@ async def lndhub_gettxs(
pending=True,
outgoing=True,
incoming=False,
limit=limit,
offset=offset,
exclude_uncheckable=True,
):
await payment.set_pending(
(await WALLET.get_payment_status(payment.checking_id)).pending
)
await asyncio.sleep(0.1)
return [
{
"payment_preimage": payment.preimage,
@@ -146,28 +155,34 @@ async def lndhub_gettxs(
complete=True,
outgoing=True,
incoming=False,
limit=limit,
offset=offset,
)
)[:limit]
)
)
]
@lndhub_ext.get("/ext/getuserinvoices")
async def lndhub_getuserinvoices(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200)
wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
):
await delete_expired_invoices()
for invoice in await get_payments(
wallet_id=wallet.wallet.id,
complete=False,
pending=True,
outgoing=False,
incoming=True,
limit=limit,
offset=offset,
exclude_uncheckable=True,
):
await invoice.set_pending(
(await WALLET.get_invoice_status(invoice.checking_id)).pending
)
await asyncio.sleep(0.1)
return [
{
@@ -190,8 +205,10 @@ async def lndhub_getuserinvoices(
complete=True,
incoming=True,
outgoing=False,
limit=limit,
offset=offset,
)
)[:limit]
)
)
]
@@ -117,9 +117,10 @@
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
{{ col.label }}
</q-th>
</q-tr>
</template>
@@ -136,9 +137,19 @@
:href="'mailto:' + props.row.email"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="ticketCard(props)"
><q-tooltip> Click to show ticket </q-tooltip></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
{{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }}
</q-td>
<q-td auto-width>
@@ -249,6 +260,29 @@
</q-form>
</q-card>
</q-dialog>
<!-- Read Ticket Dialog -->
<q-dialog v-model="ticketDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
{% raw %}
<q-card-section>
<h4 class="text-subtitle1 q-my-none">
<i>{{this.ticketDialog.data.name}}</i> sent a ticket
</h4>
<div v-if="this.ticketDialog.data.email">
<small>{{this.ticketDialog.data.email}}</small>
</div>
<small>{{this.ticketDialog.data.date}}</small>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<p>{{this.ticketDialog.data.content}}</p>
</q-card-section>
{% endraw %}
<q-card-actions align="right">
<q-btn flat label="CLOSE" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
@@ -318,6 +352,10 @@
formDialog: {
show: false,
data: {flatrate: false}
},
ticketDialog: {
show: false,
data: {}
}
}
},
@@ -372,6 +410,16 @@
})
})
},
ticketCard(ticket){
this.ticketDialog.show = true
let {date, email, ltext, name} = ticket.row
this.ticketDialog.data = {
date,
email,
content: ltext,
name
}
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
@@ -421,12 +469,13 @@
},
updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId})
console.log("LINK", link)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.description = link.description
this.formDialog.data.flatrate = link.flatrate
this.formDialog.data.flatrate = Boolean(link.flatrate)
this.formDialog.data.amount = link.amount
this.formDialog.show = true
},
+4
View File
@@ -103,6 +103,10 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist."
)
if data.sats < 1:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"0 invoices not allowed."
)
nwords = len(re.split(r"\s+", data.ltext))
+3
View File
@@ -0,0 +1,3 @@
# LNURLDevice
For offline LNURL devices
+17
View File
@@ -0,0 +1,17 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurldevice")
lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice"])
def lnurldevice_renderer():
return template_renderer(["lnbits/extensions/lnurldevice/templates"])
from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa
@@ -0,0 +1,6 @@
{
"name": "LNURLDevice",
"short_description": "For offline LNURL devices",
"icon": "point_of_sale",
"contributors": ["arcbtc"]
}
+136
View File
@@ -0,0 +1,136 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurldevice, lnurldevicepayment, lnurldevices
###############lnurldeviceS##########################
async def create_lnurldevice(
data: createLnurldevice,
) -> lnurldevices:
lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
lnurldevice_id,
lnurldevice_key,
data.title,
data.wallet,
data.currency,
data.device,
data.profit,
),
)
return await get_lnurldevice(lnurldevice_id)
async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldevices]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevices SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevice_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
return lnurldevices(**row) if row else None
async def get_lnurldevices(wallet_ids: Union[str, List[str]]) -> List[lnurldevices]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall(
f"""
SELECT * FROM lnurldevice.lnurldevices WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [lnurldevices(**row) if row else None for row in rows]
async def delete_lnurldevice(lnurldevice_id: str) -> None:
await db.execute(
"DELETE FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
)
########################lnuldevice payments###########################
async def create_lnurldevicepayment(
deviceid: str,
payload: Optional[str] = None,
pin: Optional[str] = None,
payhash: Optional[str] = None,
sats: Optional[int] = 0,
) -> lnurldevicepayment:
lnurldevicepayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payload,
pin,
payhash,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurldevicepayment_id, deviceid, payload, pin, payhash, sats),
)
return await get_lnurldevicepayment(lnurldevicepayment_id)
async def update_lnurldevicepayment(
lnurldevicepayment_id: str, **kwargs
) -> Optional[lnurldevicepayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevicepayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
)
return lnurldevicepayment(**row) if row else None
async def get_lnurldevicepayment(lnurldevicepayment_id: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?",
(lnurldevicepayment_id,),
)
return lnurldevicepayment(**row) if row else None
async def get_lnurlpayload(lnurldevicepayment_payload: str) -> lnurldevicepayment:
row = await db.fetchone(
"SELECT * FROM lnurldevice.lnurldevicepayment WHERE payload = ?",
(lnurldevicepayment_payload,),
)
return lnurldevicepayment(**row) if row else None
+231
View File
@@ -0,0 +1,231 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from lnbits.core.views.api import pay_invoice
from . import lnurldevice_ext
from .crud import (
create_lnurldevicepayment,
get_lnurldevice,
get_lnurldevicepayment,
update_lnurldevicepayment,
get_lnurlpayload,
)
def bech32_decode(bech):
"""tweaked version of bech32_decode that ignores length limitations"""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return
bech = bech.lower()
device = bech.rfind("1")
if device < 1 or device + 7 > len(bech):
return
if not all(x in bech32.CHARSET for x in bech[device + 1 :]):
return
hrp = bech[:device]
data = [bech32.CHARSET.find(x) for x in bech[device + 1 :]]
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
l = s.read(1)[0]
nonce = s.read(l)
if len(nonce) != l:
raise RuntimeError("Missing nonce bytes")
if l < 8:
raise RuntimeError("Nonce is too short")
# reading payload
l = s.read(1)[0]
payload = s.read(l)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != l:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
).digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[: len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return pin, amount_in_cent
@lnurldevice_ext.get(
"/api/v1/lnurl/{device_id}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_v1_params",
)
async def lnurl_v1_params(
request: Request,
device_id: str = Query(None),
p: str = Query(None),
atm: str = Query(None),
):
device = await get_lnurldevice(device_id)
if not device:
return {
"status": "ERROR",
"reason": f"lnurldevice {device_id} not found on this server",
}
paymentcheck = await get_lnurlpayload(p)
if device.device == "atm":
if paymentcheck:
return {"status": "ERROR", "reason": f"Payment already claimed"}
if len(p) % 4 > 0:
p += "=" * (4 - (len(p) % 4))
data = base64.urlsafe_b64decode(p)
pin = 0
amount_in_cent = 0
try:
result = xor_decrypt(device.key.encode(), data)
pin = result[0]
amount_in_cent = result[1]
except Exception as exc:
return {"status": "ERROR", "reason": str(exc)}
price_msat = (
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, device.currency)
if device.currency != "sat"
else amount_in_cent
) * 1000
if atm:
if device.device != "atm":
return {"status": "ERROR", "reason": "Not ATM device."}
price_msat = int(price_msat * (1 - (device.profit / 100)) / 1000)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "withdrawRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"k1": lnurldevicepayment.id,
"minWithdrawable": price_msat * 1000,
"maxWithdrawable": price_msat * 1000,
"defaultDescription": device.title,
}
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
print(price_msat)
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=p,
sats=price_msat * 1000,
pin=pin,
payhash="payment_hash",
)
if not lnurldevicepayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id
),
"minSendable": price_msat * 1000,
"maxSendable": price_msat * 1000,
"metadata": await device.lnurlpay_metadata(),
}
@lnurldevice_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurldevice.lnurl_callback",
)
async def lnurl_callback(
request: Request,
paymentid: str = Query(None),
pr: str = Query(None),
k1: str = Query(None),
):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found."
)
if pr:
if lnurldevicepayment.id != k1:
return {"status": "ERROR", "reason": "Bad K1"}
if lnurldevicepayment.payhash != "payment_hash":
return {"status": "ERROR", "reason": f"Payment already claimed"}
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload
)
await pay_invoice(
wallet_id=device.wallet,
payment_request=pr,
max_sat=lnurldevicepayment.sats / 1000,
extra={"tag": "withdraw"},
)
return {"status": "OK"}
print(lnurldevicepayment.sats)
payment_hash, payment_request = await create_invoice(
wallet_id=device.wallet,
amount=lnurldevicepayment.sats / 1000,
memo=device.title,
description_hash=hashlib.sha256(
(await device.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "PoS"},
)
lnurldevicepayment = await update_lnurldevicepayment(
lnurldevicepayment_id=paymentid, payhash=payment_hash
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": request.url_for("lnurldevice.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()
@@ -0,0 +1,81 @@
from lnbits.db import Database
db2 = Database("ext_lnurlpos")
async def m001_initial(db):
"""
Initial lnurldevice table.
"""
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevices (
id TEXT NOT NULL PRIMARY KEY,
key TEXT NOT NULL,
title TEXT NOT NULL,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
device TEXT NOT NULL,
profit FLOAT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE lnurldevice.lnurldevicepayment (
id TEXT NOT NULL PRIMARY KEY,
deviceid TEXT NOT NULL,
payhash TEXT,
payload TEXT NOT NULL,
pin INT,
sats INT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m002_redux(db):
"""
Moves everything from lnurlpos to lnurldevices
"""
try:
for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevices (
id,
key,
title,
wallet,
currency,
device,
profit
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3], row[4], "pos", 0),
)
for row in [
list(row)
for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpospayment")
]:
await db.execute(
"""
INSERT INTO lnurldevice.lnurldevicepayment (
id,
deviceid,
payhash,
payload,
pin,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[3], row[4], row[5], row[6]),
)
except:
return
@@ -11,33 +11,40 @@ from pydantic import BaseModel
from pydantic.main import BaseModel
class createLnurlpos(BaseModel):
class createLnurldevice(BaseModel):
title: str
wallet: str
currency: str
device: str
profit: float
class lnurlposs(BaseModel):
class lnurldevices(BaseModel):
id: str
key: str
title: str
wallet: str
currency: str
device: str
profit: float
timestamp: str
def from_row(cls, row: Row) -> "lnurlposs":
def from_row(cls, row: Row) -> "lnurldevices":
return cls(**dict(row))
def lnurl(self, req: Request) -> Lnurl:
url = req.url_for("lnurlpos.lnurl_response", pos_id=self.id, _external=True)
url = req.url_for(
"lnurldevice.lnurl_response", device_id=self.id, _external=True
)
return lnurl_encode(url)
async def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
class lnurlpospayment(BaseModel):
class lnurldevicepayment(BaseModel):
id: str
posid: str
deviceid: str
payhash: str
payload: str
pin: int
@@ -45,5 +52,5 @@ class lnurlpospayment(BaseModel):
timestamp: str
@classmethod
def from_row(cls, row: Row) -> "lnurlpospayment":
def from_row(cls, row: Row) -> "lnurldevicepayment":
return cls(**dict(row))
@@ -1,10 +1,10 @@
<q-card>
<q-card-section>
<p>
Register LNURLPoS devices to receive payments in your LNbits wallet.<br />
Register LNURLDevice devices to receive payments in your LNbits wallet.<br />
Build your own here
<a href="https://github.com/arcbtc/LNURLPoS"
>https://github.com/arcbtc/LNURLPoS</a
<a href="https://github.com/arcbtc/bitcoinpos"
>https://github.com/arcbtc/bitcoinpos</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
@@ -21,12 +21,12 @@
group="api"
dense
expand-separator
label="Create lnurlpos"
label="Create lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /lnurlpos/api/v1/lnurlpos</code
><span class="text-blue"></span> /lnurldevice/api/v1/lnurlpos</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@@ -36,10 +36,10 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_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/lnurlpos -d '{"title":
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
@@ -51,13 +51,13 @@
group="api"
dense
expand-separator
label="Update lnurlpos"
label="Update lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@@ -67,25 +67,30 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_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/lnurlpos/&lt;lnurlpos_id&gt; -d ''{"title": &lt;string&gt;,
"message":&lt;string&gt;, "currency": &lt;integer&gt;} -H
"Content-type: application/json" -H "X-Api-Key:
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -d ''{"title":
&lt;string&gt;, "message":&lt;string&gt;, "currency":
&lt;integer&gt;} -H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get lnurlpos">
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevice"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@@ -95,21 +100,27 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_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/lnurlpos/&lt;lnurlpos_id&gt; -H "X-Api-Key: {{
}}api/v1/lnurlpos/&lt;lnurldevice_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 lnurlposs">
<q-expansion-item
group="api"
dense
expand-separator
label="Get lnurldevices"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /lnurlpos/api/v1/lnurlposs</code
><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlposs</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
@@ -119,11 +130,11 @@
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpos_object&gt;, ...]</code>
<code>[&lt;lnurldevice_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/lnurlposs -H "X-Api-Key:
{{ user.wallets[0].inkey }}"
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
@@ -139,7 +150,7 @@
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurlpos/api/v1/lnurlpos/&lt;lnurlpos_id&gt;</code
/lnurldevice/api/v1/lnurlpos/&lt;lnurldevice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@@ -148,7 +159,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/lnurlpos/&lt;lnurlpos_id&gt; -H "X-Api-Key: {{
}}api/v1/lnurlpos/&lt;lnurldevice_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
@@ -8,8 +8,8 @@
<q-btn
unelevated
color="primary"
@click="formDialoglnurlpos.show = true"
>New LNURLPoS instance
@click="formDialoglnurldevice.show = true"
>New LNURLDevice instance
</q-btn>
</q-card-section>
</q-card>
@@ -18,7 +18,7 @@
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">lNURLPoS</h5>
<h5 class="text-subtitle1 q-my-none">lNURLdevice</h5>
</div>
<div class="col-auto">
@@ -33,7 +33,7 @@
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurlposCSV"
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
>Export to CSV</q-btn
>
</div>
@@ -41,10 +41,10 @@
<q-table
flat
dense
:data="lnurlposLinks"
:data="lnurldeviceLinks"
row-key="id"
:columns="lnurlpossTable.columns"
:pagination.sync="lnurlpossTable.pagination"
:columns="lnurldevicesTable.columns"
:pagination.sync="lnurldevicesTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
@@ -72,11 +72,11 @@
flat
dense
size="xs"
@click="deletelnurlposLink(props.row.id)"
@click="deletelnurldeviceLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete LNURLPoS </q-tooltip>
<q-tooltip> Delete LNURLDevice </q-tooltip>
</q-btn>
</q-td>
<q-td>
@@ -84,11 +84,11 @@
flat
dense
size="xs"
@click="openlnurlposSettings(props.row.id)"
@click="openlnurldeviceSettings(props.row.id)"
icon="perm_data_setting"
color="primary"
>
<q-tooltip> LNURLPoS Settings </q-tooltip>
<q-tooltip> LNURLDevice Settings </q-tooltip>
</q-btn>
</q-td>
<q-td
@@ -112,46 +112,57 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLPoS Extension
{{SITE_TITLE}} LNURLDevice Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "lnurlpos/_api_docs.html" %} </q-list>
<q-list> {% include "lnurldevice/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="settingsDialog.show"
position="top"
deviceition="top"
@hide="closeFormDialog"
>
<q-card
style="width: 700px; max-width: 80vw"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<div class="text-h6">Copy to LNURLPoS device</div>
<div class="text-h6">LNURLDevice device string</div>
<q-btn
dense
outline
unelevated
color="primary"
size="md"
@click="copyText(location + '/lnurldevice/api/v1/lnurl/' + settingsDialog.data.id + ',' +
settingsDialog.data.key + ',' + settingsDialog.data.currency, 'Link copied to clipboard!')"
>{% raw
%}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}},
{{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw
%}<q-tooltip> Click to copy URL </q-tooltip>
</q-btn>
<div class="text-subtitle2">
{% raw %} String baseURL =
"{{location}}/lnurlpos/api/v1/lnurl/{{settingsDialog.data.id}}";<br />
String key = "{{settingsDialog.data.key}}";<br />
String currency = "{{settingsDialog.data.currency}}";{% endraw %}
<small> </small>
</div>
</q-card>
</q-dialog>
<q-dialog
v-model="formDialoglnurlpos.show"
position="top"
v-model="formDialoglnurldevice.show"
deviceition="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDatalnurlpos" class="q-gutter-md">
<q-form @submit="sendFormDatalnurldevice" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialoglnurlpos.data.title"
v-model.trim="formDialoglnurldevice.data.title"
type="text"
label="Title"
></q-input>
@@ -160,7 +171,7 @@
filled
dense
emit-value
v-model="formDialoglnurlpos.data.wallet"
v-model="formDialoglnurldevice.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
@@ -168,32 +179,46 @@
<q-select
filled
dense
v-model.trim="formDialoglnurlpos.data.currency"
v-model.trim="formDialoglnurldevice.data.currency"
type="text"
label="Fiat currency for PoS"
label="Fiat currency for device"
:options="currency"
></q-select>
<q-option-group
v-model.trim="formDialoglnurldevice.data.device"
:options="devices"
color="primary"
label="Type of device"
></q-option-group>
<q-input
filled
dense
v-model.trim="formDialoglnurldevice.data.profit"
type="number"
max="90"
label="Profit margin (% added to invoices/deducted from faucets)"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="formDialoglnurlpos.data.id"
v-if="formDialoglnurldevice.data.id"
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
formDialoglnurldevice.data.title == ''"
type="submit"
>Update lnurlpos</q-btn
>Update lnurldevice</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialoglnurlpos.data.title == ''"
formDialoglnurldevice.data.title == ''"
type="submit"
>Create lnurlpos</q-btn
>Create lnurldevice</q-btn
>
<q-btn @click="cancellnurlpos" flat color="grey" class="q-ml-auto"
<q-btn @click="cancellnurldevice" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
@@ -206,7 +231,7 @@
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplnurlpos = obj => {
var maplnurldevice = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
@@ -230,9 +255,19 @@
location: window.location.hostname,
filter: '',
currency: 'USD',
lnurlposLinks: [],
lnurlposLinksObj: [],
lnurlpossTable: {
lnurldeviceLinks: [],
lnurldeviceLinksObj: [],
devices: [
{
label: 'PoS',
value: 'pos'
},
{
label: 'ATM',
value: 'atm'
}
],
lnurldevicesTable: {
columns: [
{
name: 'title',
@@ -258,6 +293,18 @@
label: 'wallet',
field: 'wallet'
},
{
name: 'device',
align: 'left',
label: 'device',
field: 'device'
},
{
name: 'profit',
align: 'left',
label: 'profit',
field: 'profit'
},
{
name: 'currency',
align: 'left',
@@ -269,7 +316,7 @@
rowsPerPage: 10
}
},
passedlnurlpos: {},
passedlnurldevice: {},
settingsDialog: {
show: false,
data: {}
@@ -278,13 +325,15 @@
show: false,
data: {}
},
formDialoglnurlpos: {
formDialoglnurldevice: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
device: 'pos',
profit: 2,
title: ''
}
},
@@ -295,33 +344,33 @@
}
},
methods: {
cancellnurlpos: function (data) {
cancellnurldevice: function (data) {
var self = this
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
},
closeFormDialog: function () {
this.clearFormDialoglnurlpos()
this.clearFormDialoglnurldevice()
this.formDialog.data = {
is_unique: false
}
},
sendFormDatalnurlpos: function () {
sendFormDatalnurldevice: function () {
var self = this
if (self.formDialoglnurlpos.data.id) {
this.updatelnurlpos(
if (self.formDialoglnurldevice.data.id) {
this.updatelnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
self.formDialoglnurldevice.data
)
} else {
this.createlnurlpos(
this.createlnurldevice(
self.g.user.wallets[0].adminkey,
self.formDialoglnurlpos.data
self.formDialoglnurldevice.data
)
}
},
createlnurlpos: function (wallet, data) {
createlnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
@@ -330,86 +379,91 @@
}
}
LNbits.api
.request('POST', '/lnurlpos/api/v1/lnurlpos', wallet, updatedData)
.request('POST', '/lnurldevice/api/v1/lnurlpos', wallet, updatedData)
.then(function (response) {
self.lnurlposLinks.push(maplnurlpos(response.data))
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlposs: function () {
getlnurldevices: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos',
'/lnurldevice/api/v1/lnurlpos',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
self.lnurlposLinks = response.data.map(maplnurlpos)
self.lnurldeviceLinks = response.data.map(maplnurldevice)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getlnurlpos: function (lnurlpos_id) {
getlnurldevice: function (lnurldevice_id) {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpos/api/v1/lnurlpos/' + lnurlpos_id,
'/lnurldevice/api/v1/lnurlpos/' + lnurldevice_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
localStorage.setItem('lnurlpos', JSON.stringify(response.data))
localStorage.setItem('lnurldevice', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurlposLink: function (lnurlposId) {
deletelnurldeviceLink: function (lnurldeviceId) {
var self = this
var link = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
var link = _.findWhere(this.lnurldeviceLinks, {id: lnurldeviceId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurlpos/api/v1/lnurlpos/' + lnurlposId,
'/lnurldevice/api/v1/lnurlpos/' + lnurldeviceId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (
obj
) {
return obj.id === lnurlposId
})
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === lnurldeviceId
}
)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdatelnurlposLink: function (lnurlposId) {
openUpdatelnurldeviceLink: function (lnurldeviceId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.formDialoglnurlpos.data = _.clone(lnurlpos._data)
self.formDialoglnurlpos.show = true
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.formDialoglnurldevice.data = _.clone(lnurldevice._data)
self.formDialoglnurldevice.show = true
},
openlnurlposSettings: function (lnurlposId) {
openlnurldeviceSettings: function (lnurldeviceId) {
var self = this
var lnurlpos = _.findWhere(this.lnurlposLinks, {id: lnurlposId})
self.settingsDialog.data = _.clone(lnurlpos._data)
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
id: lnurldeviceId
})
self.settingsDialog.data = _.clone(lnurldevice._data)
self.settingsDialog.show = true
},
updatelnurlpos: function (wallet, data) {
updatelnurldevice: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
@@ -421,24 +475,27 @@
LNbits.api
.request(
'PUT',
'/lnurlpos/api/v1/lnurlpos/' + updatedData.id,
'/lnurldevice/api/v1/lnurlpos/' + updatedData.id,
wallet,
updatedData
)
.then(function (response) {
self.lnurlposLinks = _.reject(self.lnurlposLinks, function (obj) {
return obj.id === updatedData.id
})
self.lnurlposLinks.push(maplnurlpos(response.data))
self.formDialoglnurlpos.show = false
self.clearFormDialoglnurlpos()
self.lnurldeviceLinks = _.reject(
self.lnurldeviceLinks,
function (obj) {
return obj.id === updatedData.id
}
)
self.lnurldeviceLinks.push(maplnurldevice(response.data))
self.formDialoglnurldevice.show = false
self.clearFormDialoglnurldevice()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
clearFormDialoglnurlpos() {
this.formDialoglnurlpos.data = {
clearFormDialoglnurldevice() {
this.formDialoglnurldevice.data = {
lnurl_toggle: false,
show_message: false,
show_ack: false,
@@ -446,15 +503,18 @@
title: ''
}
},
exportlnurlposCSV: function () {
exportlnurldeviceCSV: function () {
var self = this
LNbits.utils.exportCSV(self.lnurlpossTable.columns, this.lnurlposLinks)
LNbits.utils.exportCSV(
self.lnurldevicesTable.columns,
this.lnurldeviceLinks
)
}
},
created: function () {
var self = this
var getlnurlposs = this.getlnurlposs
getlnurlposs()
var getlnurldevices = this.getlnurldevices
getlnurldevices()
self.location = [
window.location.protocol,
'//',
+53
View File
@@ -0,0 +1,53 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import lnurldevice_ext, lnurldevice_renderer
from .crud import get_lnurldevice, get_lnurldevicepayment
templates = Jinja2Templates(directory="templates")
@lnurldevice_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/index.html", {"request": request, "user": user.dict()}
)
@lnurldevice_ext.get(
"/{paymentid}", name="lnurldevice.displaypin", response_class=HTMLResponse
)
async def displaypin(request: Request, paymentid: str = Query(None)):
lnurldevicepayment = await get_lnurldevicepayment(paymentid)
if not lnurldevicepayment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No lmurldevice payment"
)
device = await get_lnurldevice(lnurldevicepayment.deviceid)
if not device:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice not found."
)
status = await api_payment(lnurldevicepayment.payhash)
if status["paid"]:
await update_payment_status(
checking_id=lnurldevicepayment.payhash, pending=True
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/paid.html", {"request": request, "pin": lnurldevicepayment.pin}
)
return lnurldevice_renderer().TemplateResponse(
"lnurldevice/error.html",
{"request": request, "pin": "filler", "not_paid": True},
)
@@ -0,0 +1,88 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurldevice import lnurldevice_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurldevice_ext
from .crud import (
create_lnurldevice,
delete_lnurldevice,
get_lnurldevice,
get_lnurldevices,
update_lnurldevice,
)
from .models import createLnurldevice
@lnurldevice_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
#######################lnurldevice##########################
@lnurldevice_ext.post("/api/v1/lnurlpos")
@lnurldevice_ext.put("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_create_or_update(
data: createLnurldevice,
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
):
if not lnurldevice_id:
lnurldevice = await create_lnurldevice(data)
return lnurldevice.dict()
else:
lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id)
return lnurldevice.dict()
@lnurldevice_ext.get("/api/v1/lnurlpos")
async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [
{**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids)
]
except:
return ""
@lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_retrieve(
request: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurldevice does not exist"
)
if not lnurldevice.lnurl_toggle:
return {**lnurldevice.dict()}
return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(request=request)}}
@lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}")
async def api_lnurldevice_delete(
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurldevice_id: str = Query(None),
):
lnurldevice = await get_lnurldevice(lnurldevice_id)
if not lnurldevice:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurldevice(lnurldevice_id)
return "", HTTPStatus.NO_CONTENT
+6 -1
View File
@@ -66,7 +66,7 @@ async def api_link_retrieve(
detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN
)
return {**link._asdict(), **{"lnurl": link.lnurl(r)}}
return {**link.dict(), **{"lnurl": link.lnurl(r)}}
@lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED)
@@ -76,6 +76,11 @@ async def api_link_create_or_update(
link_id=None,
wallet: WalletTypeInfo = Depends(get_key_type),
):
if data.min < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.min > data.max:
raise HTTPException(
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
@@ -13,10 +13,12 @@ lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout
def lnurlpayout_renderer():
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def lnurlpayout_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+22 -5
View File
@@ -6,14 +6,23 @@ from . import db
from .models import lnurlpayout, CreateLnurlPayoutData
async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPayoutData) -> lnurlpayout:
async def create_lnurlpayout(
wallet_id: str, admin_key: str, data: CreateLnurlPayoutData
) -> lnurlpayout:
lnurlpayout_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpayout.lnurlpayouts (id, title, wallet, admin_key, lnurlpay, threshold)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurlpayout_id, data.title, wallet_id, admin_key, data.lnurlpay, data.threshold),
(
lnurlpayout_id,
data.title,
wallet_id,
admin_key,
data.lnurlpay,
data.threshold,
),
)
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
@@ -22,13 +31,19 @@ async def create_lnurlpayout(wallet_id: str, admin_key: str, data: CreateLnurlPa
async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)
return lnurlpayout(**row) if row else None
async def get_lnurlpayout_from_wallet(wallet_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,))
row = await db.fetchone(
"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet = ?", (wallet_id,)
)
return lnurlpayout(**row) if row else None
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
@@ -42,4 +57,6 @@ async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayou
async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
await db.execute(
"DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,)
)
+2
View File
@@ -2,11 +2,13 @@ from sqlite3 import Row
from pydantic import BaseModel
class CreateLnurlPayoutData(BaseModel):
title: str
lnurlpay: str
threshold: int
class lnurlpayout(BaseModel):
id: str
title: str
+38 -19
View File
@@ -1,15 +1,17 @@
import asyncio
import json
from http import HTTPStatus
import httpx
from starlette.exceptions import HTTPException
from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from lnbits.core.views.api import api_wallet
from lnbits.core.crud import get_wallet
from lnbits.core.views.api import api_payment, api_payments_decode, pay_invoice
from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice
from lnbits.core.views.api import api_payments_decode
from lnbits.tasks import register_invoice_listener
from .crud import get_lnurlpayout, get_lnurlpayout_from_wallet
from .crud import get_lnurlpayout_from_wallet
async def wait_for_paid_invoices():
@@ -25,32 +27,44 @@ async def on_invoice_paid(payment: Payment) -> None:
try:
# Check its got a payout associated with it
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
print("LNURLpayout", lnurlpayout_link)
if lnurlpayout_link:
# Check the wallet balance is more than the threshold
wallet = await get_wallet(lnurlpayout_link.wallet)
if wallet.balance < lnurlpayout_link.threshold + (lnurlpayout_link.threshold*0.02):
threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02)
if wallet.balance < threshold:
return
# Get the invoice from the LNURL to pay
async with httpx.AsyncClient() as client:
try:
url = await api_payments_decode({"data": lnurlpayout_link.lnurlpay})
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken")
try:
r = await client.get(
str(url["domain"]),
timeout=40,
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
)
try:
r = await client.get(str(url["domain"]), timeout=40)
res = r.json()
try:
r = await client.get(
res["callback"] + "?amount=" + str(int((wallet.balance - wallet.balance*0.02) * 1000)),
res["callback"]
+ "?amount="
+ str(
int((wallet.balance - wallet.balance * 0.02) * 1000)
),
timeout=40,
)
res = r.json()
if hasattr(res, "status") and res["status"] == "ERROR":
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=res["reason"],
)
try:
await pay_invoice(
wallet_id=payment.wallet_id,
@@ -60,11 +74,16 @@ async def on_invoice_paid(payment: Payment) -> None:
return
except:
pass
except:
except Exception as e:
print("ERROR", str(e))
return
except (httpx.ConnectError, httpx.RequestError):
return
except Exception:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to save LNURLPayout",
)
except:
return
return
+56 -20
View File
@@ -4,16 +4,24 @@ from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, get_payments
from lnbits.core.crud import get_payments, get_user
from lnbits.core.models import Payment
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment, api_payments_decode
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import lnurlpayout_ext
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts, get_lnurlpayout_from_wallet
from .models import lnurlpayout, CreateLnurlPayoutData
from .crud import (
create_lnurlpayout,
delete_lnurlpayout,
get_lnurlpayout,
get_lnurlpayout_from_wallet,
get_lnurlpayouts,
)
from .models import CreateLnurlPayoutData, lnurlpayout
from .tasks import on_invoice_paid
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
async def api_lnurlpayouts(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
@@ -28,23 +36,33 @@ async def api_lnurlpayouts(
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
async def api_lnurlpayout_create(
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
):
):
if await get_lnurlpayout_from_wallet(wallet.wallet.id):
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Wallet already has lnurlpayout set")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Wallet already has lnurlpayout set",
)
return
url = await api_payments_decode({"data": data.lnurlpay})
if "domain" not in url:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL could not be decoded"
)
return
if str(url["domain"])[0:4] != "http":
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not valid LNURL")
return
lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data)
lnurlpayout = await create_lnurlpayout(
wallet_id=wallet.wallet.id, admin_key=wallet.wallet.adminkey, data=data
)
if not lnurlpayout:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to save LNURLPayout"
)
return
return lnurlpayout.dict()
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
async def api_lnurlpayout_delete(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@@ -57,26 +75,44 @@ async def api_lnurlpayout_delete(
)
if lnurlpayout.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.")
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout."
)
await delete_lnurlpayout(lnurlpayout_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@lnurlpayout_ext.get("/api/v1/lnurlpayouts/{lnurlpayout_id}", status_code=HTTPStatus.OK)
async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
payments = await get_payments(
wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
## THIS
mock_payment = Payment(
checking_id="mock",
pending=False,
amount=1,
fee=1,
time=0000,
bolt11="mock",
preimage="mock",
payment_hash="mock",
wallet_id=lnurlpayout.wallet,
)
result = await on_invoice_paid(payments[0])
## INSTEAD OF THIS
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
result = await on_invoice_paid(mock_payment)
return
# get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# for lnurlpayout in lnurlpayouts:
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
# await on_invoice_paid(payments[0])
# get payouts func
# lnurlpayouts = await get_lnurlpayouts(wallet_ids)
# for lnurlpayout in lnurlpayouts:
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
# await on_invoice_paid(payments[0])
-3
View File
@@ -1,3 +0,0 @@
# LNURLPoS
For offline LNURL PoS devices
-17
View File
@@ -1,17 +0,0 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurlpos")
lnurlpos_ext: APIRouter = APIRouter(prefix="/lnurlpos", tags=["lnurlpos"])
def lnurlpos_renderer():
return template_renderer(["lnbits/extensions/lnurlpos/templates"])
from .lnurl import * # noqa
from .views import * # noqa
from .views_api import * # noqa
-6
View File
@@ -1,6 +0,0 @@
{
"name": "LNURLPoS",
"short_description": "For offline LNURL PoS systems",
"icon": "point_of_sale",
"contributors": ["arcbtc"]
}
-112
View File
@@ -1,112 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import createLnurlpos, lnurlpospayment, lnurlposs
###############lnurlposS##########################
async def create_lnurlpos(data: createLnurlpos,) -> lnurlposs:
lnurlpos_id = urlsafe_short_hash()
lnurlpos_key = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpos.lnurlposs (
id,
key,
title,
wallet,
currency
)
VALUES (?, ?, ?, ?, ?)
""",
(lnurlpos_id, lnurlpos_key, data.title, data.wallet, data.currency),
)
return await get_lnurlpos(lnurlpos_id)
async def update_lnurlpos(lnurlpos_id: str, **kwargs) -> Optional[lnurlposs]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurlpos.lnurlposs SET {q} WHERE id = ?",
(*kwargs.values(), lnurlpos_id),
)
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
)
return lnurlposs(**row) if row else None
async def get_lnurlpos(lnurlpos_id: str) -> lnurlposs:
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,)
)
return lnurlposs(**row) if row else None
async def get_lnurlposs(wallet_ids: Union[str, List[str]]) -> List[lnurlposs]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids[0]))
rows = await db.fetchall(
f"""
SELECT * FROM lnurlpos.lnurlposs WHERE wallet IN ({q})
ORDER BY id
""",
(*wallet_ids,),
)
return [lnurlposs(**row) if row else None for row in rows]
async def delete_lnurlpos(lnurlpos_id: str) -> None:
await db.execute("DELETE FROM lnurlpos.lnurlposs WHERE id = ?", (lnurlpos_id,))
########################lnulpos payments###########################
async def create_lnurlpospayment(
posid: str,
payload: Optional[str] = None,
pin: Optional[str] = None,
payhash: Optional[str] = None,
sats: Optional[int] = 0,
) -> lnurlpospayment:
lnurlpospayment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpos.lnurlpospayment (
id,
posid,
payload,
pin,
payhash,
sats
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(lnurlpospayment_id, posid, payload, pin, payhash, sats),
)
return await get_lnurlpospayment(lnurlpospayment_id)
async def update_lnurlpospayment(
lnurlpospayment_id: str, **kwargs
) -> Optional[lnurlpospayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurlpos.lnurlpospayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurlpospayment_id),
)
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
)
return lnurlpospayment(**row) if row else None
async def get_lnurlpospayment(lnurlpospayment_id: str) -> lnurlpospayment:
row = await db.fetchone(
"SELECT * FROM lnurlpos.lnurlpospayment WHERE id = ?", (lnurlpospayment_id,)
)
return lnurlpospayment(**row) if row else None
-169
View File
@@ -1,169 +0,0 @@
import base64
import hashlib
from http import HTTPStatus
from typing import Optional
from embit import bech32
from embit import compact
import base64
from io import BytesIO
import hmac
from fastapi import Request
from fastapi.param_functions import Query
from starlette.exceptions import HTTPException
from lnbits.core.services import create_invoice
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
from . import lnurlpos_ext
from .crud import (
create_lnurlpospayment,
get_lnurlpos,
get_lnurlpospayment,
update_lnurlpospayment,
)
def bech32_decode(bech):
"""tweaked version of bech32_decode that ignores length limitations"""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech):
return
if not all(x in bech32.CHARSET for x in bech[pos+1:]):
return
hrp = bech[:pos]
data = [bech32.CHARSET.find(x) for x in bech[pos+1:]]
encoding = bech32.bech32_verify_checksum(hrp, data)
if encoding is None:
return
return bytes(bech32.convertbits(data[:-6], 5, 8, False))
def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
l = s.read(1)[0]
nonce = s.read(l)
if len(nonce) != l:
raise RuntimeError("Missing nonce bytes")
if l < 8:
raise RuntimeError("Nonce is too short")
# reading payload
l = s.read(1)[0]
payload = s.read(l)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != l:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(key, b"Data:" + blob[:-len(hmacval)], digestmod="sha256").digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[:len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return pin, amount_in_cent
@lnurlpos_ext.get(
"/api/v1/lnurl/{pos_id}",
status_code=HTTPStatus.OK,
name="lnurlpos.lnurl_v1_params",
)
async def lnurl_v1_params(
request: Request,
pos_id: str = Query(None),
p: str = Query(None),
):
return await handle_lnurl_firstrequest(request, pos_id, p)
async def handle_lnurl_firstrequest(
request: Request, pos_id: str, payload: str
):
pos = await get_lnurlpos(pos_id)
if not pos:
return {
"status": "ERROR",
"reason": f"lnurlpos {pos_id} not found on this server",
}
if len(payload) % 4 > 0:
payload += "="*(4-(len(payload)%4))
data = base64.urlsafe_b64decode(payload)
pin, amount_in_cent = xor_decrypt(pos.key.encode(), data)
price_msat = (
await fiat_amount_as_satoshis(float(amount_in_cent) / 100, pos.currency)
if pos.currency != "sat"
else amount
) * 1000
lnurlpospayment = await create_lnurlpospayment(
posid=pos.id,
payload=payload,
sats=price_msat,
pin=pin,
payhash="payment_hash",
)
if not lnurlpospayment:
return {"status": "ERROR", "reason": "Could not create payment."}
return {
"tag": "payRequest",
"callback": request.url_for(
"lnurlpos.lnurl_callback", paymentid=lnurlpospayment.id
),
"minSendable": price_msat,
"maxSendable": price_msat,
"metadata": await pos.lnurlpay_metadata(),
}
@lnurlpos_ext.get(
"/api/v1/lnurl/cb/{paymentid}",
status_code=HTTPStatus.OK,
name="lnurlpos.lnurl_callback",
)
async def lnurl_callback(request: Request, paymentid: str = Query(None)):
lnurlpospayment = await get_lnurlpospayment(paymentid)
pos = await get_lnurlpos(lnurlpospayment.posid)
if not pos:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="lnurlpos not found."
)
payment_hash, payment_request = await create_invoice(
wallet_id=pos.wallet,
amount=int(lnurlpospayment.sats / 1000),
memo=pos.title,
description_hash=hashlib.sha256(
(await pos.lnurlpay_metadata()).encode("utf-8")
).digest(),
extra={"tag": "lnurlpos"},
)
lnurlpospayment = await update_lnurlpospayment(
lnurlpospayment_id=paymentid, payhash=payment_hash
)
return {
"pr": payment_request,
"successAction": {
"tag": "url",
"description": "Check the attached link",
"url": request.url_for("lnurlpos.displaypin", paymentid=paymentid),
},
"routes": [],
}
return resp.dict()
-30
View File
@@ -1,30 +0,0 @@
async def m001_initial(db):
"""
Initial lnurlpos table.
"""
await db.execute(
f"""
CREATE TABLE lnurlpos.lnurlposs (
id TEXT NOT NULL PRIMARY KEY,
key TEXT NOT NULL,
title TEXT NOT NULL,
wallet TEXT NOT NULL,
currency TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
f"""
CREATE TABLE lnurlpos.lnurlpospayment (
id TEXT NOT NULL PRIMARY KEY,
posid TEXT NOT NULL,
payhash TEXT,
payload TEXT NOT NULL,
pin INT,
sats INT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
-50
View File
@@ -1,50 +0,0 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from . import lnurlpos_ext, lnurlpos_renderer
from .crud import get_lnurlpos, get_lnurlpospayment
templates = Jinja2Templates(directory="templates")
@lnurlpos_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/index.html", {"request": request, "user": user.dict()}
)
@lnurlpos_ext.get(
"/{paymentid}", name="lnurlpos.displaypin", response_class=HTMLResponse
)
async def displaypin(request: Request, paymentid: str = Query(None)):
lnurlpospayment = await get_lnurlpospayment(paymentid)
if not lnurlpospayment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No lmurlpos payment"
)
pos = await get_lnurlpos(lnurlpospayment.posid)
if not pos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos not found."
)
status = await api_payment(lnurlpospayment.payhash)
if status["paid"]:
await update_payment_status(checking_id=lnurlpospayment.payhash, pending=True)
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/paid.html", {"request": request, "pin": lnurlpospayment.pin}
)
return lnurlpos_renderer().TemplateResponse(
"lnurlpos/error.html", {"request": request, "pin": "filler", "not_paid": True}
)
-85
View File
@@ -1,85 +0,0 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.extensions.lnurlpos import lnurlpos_ext
from lnbits.utils.exchange_rates import currencies
from . import lnurlpos_ext
from .crud import (
create_lnurlpos,
delete_lnurlpos,
get_lnurlpos,
get_lnurlposs,
update_lnurlpos,
)
from .models import createLnurlpos
@lnurlpos_ext.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
#######################lnurlpos##########################
@lnurlpos_ext.post("/api/v1/lnurlpos")
@lnurlpos_ext.put("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_create_or_update(
data: createLnurlpos,
wallet: WalletTypeInfo = Depends(require_admin_key),
lnurlpos_id: str = Query(None),
):
if not lnurlpos_id:
lnurlpos = await create_lnurlpos(data)
return lnurlpos.dict()
else:
lnurlpos = await update_lnurlpos(data, lnurlpos_id=lnurlpos_id)
return lnurlpos.dict()
@lnurlpos_ext.get("/api/v1/lnurlpos")
async def api_lnurlposs_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
try:
return [{**lnurlpos.dict()} for lnurlpos in await get_lnurlposs(wallet_ids)]
except:
return ""
@lnurlpos_ext.get("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_retrieve(
request: Request,
wallet: WalletTypeInfo = Depends(get_key_type),
lnurlpos_id: str = Query(None),
):
lnurlpos = await get_lnurlpos(lnurlpos_id)
if not lnurlpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpos does not exist"
)
if not lnurlpos.lnurl_toggle:
return {**lnurlpos.dict()}
return {**lnurlpos.dict(), **{"lnurl": lnurlpos.lnurl(request=request)}}
@lnurlpos_ext.delete("/api/v1/lnurlpos/{lnurlpos_id}")
async def api_lnurlpos_delete(
wallet: WalletTypeInfo = Depends(require_admin_key), lnurlpos_id: str = Query(None)
):
lnurlpos = await get_lnurlpos(lnurlpos_id)
if not lnurlpos:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist."
)
await delete_lnurlpos(lnurlpos_id)
return "", HTTPStatus.NO_CONTENT
+2 -2
View File
@@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"):
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
counter = struct.pack(">Q", counter)
mac = hmac.new(key, counter, digest).digest()
offset = mac[-1] & 0x0F
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
offset = mac[-1] & 0x0f
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff
return str(binary)[-digits:].zfill(digits)
@@ -77,8 +77,8 @@
mixins: [windowMixin],
data: function () {
return {
userAmount: {{ paywall.amount }},
paywallAmount: {{ paywall.amount }},
userAmount: '{{ paywall.amount }}',
paywallAmount: '{{ paywall.amount }}',
paymentReq: null,
redirectUrl: null,
paymentDialog: {
@@ -89,7 +89,9 @@
},
computed: {
amount: function () {
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount
return this.paywallAmount > this.userAmount
? this.paywallAmount
: this.userAmount
}
},
methods: {
@@ -102,48 +104,55 @@
},
createInvoice: function () {
var self = this
console.log(this.amount)
axios
.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice',
{amount: self.amount}
LNbits.api
.request(
'POST',
'/paywall/api/v1/paywalls/invoice/{{ paywall.id }}',
'filler',
{
amount: self.amount
}
)
.then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase()
if (response.data) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.checker = setInterval(function () {
axios
.post(
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice',
{payment_hash: response.data.payment_hash}
)
.then(function (res) {
if (res.data.paid) {
self.cancelPayment()
self.redirectUrl = res.data.url
if (res.data.remembers) {
self.$q.localStorage.set(
'lnbits.paywall.{{ paywall.id }}',
res.data.url
)
self.paymentDialog.checker = setInterval(function () {
LNbits.api
.request(
'POST',
'/paywall/api/v1/paywalls/check_invoice/{{ paywall.id }}',
'filler',
{payment_hash: response.data.payment_hash}
)
.then(function (response) {
if (response.data) {
if (response.data.paid) {
self.cancelPayment()
self.redirectUrl = response.data.url
if (response.data.remembers) {
self.$q.localStorage.set(
'lnbits.paywall.{{ paywall.id }}',
response.data.url
)
}
self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
}
self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
+4 -9
View File
@@ -52,20 +52,17 @@ async def api_paywall_delete(
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/invoice")
@paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
async def api_paywall_create_invoice(
paywall_id,
data: CreatePaywallInvoice,
wallet: WalletTypeInfo = Depends(get_key_type),
paywall_id: str = Query(None)
):
paywall = await get_paywall(paywall_id)
if data.amount < paywall.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Minimum amount is {paywall.amount} sat.",
)
try:
amount = data.amount if data.amount > paywall.amount else paywall.amount
payment_hash, payment_request = await create_invoice(
@@ -80,15 +77,14 @@ async def api_paywall_create_invoice(
return {"payment_hash": payment_hash, "payment_request": payment_request}
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice")
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
@paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)):
paywall = await get_paywall(paywall_id)
payment_hash = data.payment_hash
if not paywall:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
)
try:
status = await check_invoice_status(paywall.wallet, payment_hash)
is_paid = not status.pending
@@ -101,5 +97,4 @@ async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
await payment.set_pending(False)
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
return {"paid": False}
+9
View File
@@ -1,7 +1,10 @@
import asyncio
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_satspay")
@@ -13,5 +16,11 @@ def satspay_renderer():
return template_renderer(["lnbits/extensions/satspay/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def satspay_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+4 -1
View File
@@ -25,7 +25,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
onchainaddress = None
if data.lnbitswallet:
payment_hash, payment_request = await create_invoice(
wallet_id=data.lnbitswallet, amount=data.amount, memo=charge_id
wallet_id=data.lnbitswallet,
amount=data.amount,
memo=charge_id,
extra={"tag": "charge"},
)
else:
payment_hash = None
+4 -3
View File
@@ -1,8 +1,9 @@
import time
from sqlite3 import Row
from typing import Optional
from fastapi.param_functions import Query
from pydantic import BaseModel
import time
class CreateCharge(BaseModel):
@@ -23,8 +24,8 @@ class Charges(BaseModel):
onchainwallet: Optional[str]
onchainaddress: Optional[str]
lnbitswallet: Optional[str]
payment_request: str
payment_hash: str
payment_request: Optional[str]
payment_hash: Optional[str]
webhook: Optional[str]
completelink: Optional[str]
completelinktext: Optional[str] = "Back to Merchant"
+30
View File
@@ -0,0 +1,30 @@
import asyncio
from lnbits.core.models import Payment
from lnbits.extensions.satspay.crud import check_address_balance, get_charge
from lnbits.tasks import register_invoice_listener
# from .crud import get_ticket, set_ticket_paid
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue)
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if "charge" != payment.extra.get("tag"):
# not a charge invoice
return
charge = await get_charge(payment.memo)
if not charge:
print("this should never happen", payment)
return
await payment.set_pending(False)
await check_address_balance(charge_id=charge.id)
@@ -64,7 +64,7 @@
label="lightning⚡"
>
<q-tooltip>
bitcoin onchain payment method not available
bitcoin lightning payment method not available
</q-tooltip>
</q-btn>
<q-btn
@@ -86,7 +86,7 @@
label="onchain⛓️"
>
<q-tooltip>
bitcoin lightning payment method not available
bitcoin onchain payment method not available
</q-tooltip>
</q-btn>
<q-btn
@@ -132,7 +132,7 @@
lightning-network invoice</span
>
</center>
<a href="lightning://{{ charge.payment_request }}">
<a href="lightning:{{ charge.payment_request }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="'{{ charge.payment_request }}'"
@@ -182,7 +182,7 @@
to this onchain address</span
>
</center>
<a href="bitcoin://{{ charge.onchainaddress }}">
<a href="bitcoin:{{ charge.onchainaddress }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="'{{ charge.onchainaddress }}'"
@@ -235,10 +235,24 @@
charge_time_elapsed: '{{charge.time_elapsed}}',
charge_amount: '{{charge.amount}}',
charge_balance: '{{charge.balance}}',
charge_paid: '{{charge.paid}}'
charge_paid: '{{charge.paid}}',
wallet: {
inkey: ''
},
cancelListener: () => {}
}
},
methods: {
startPaymentNotifier() {
this.cancelListener()
this.cancelListener = LNbits.event.onInvoicePaid(
this.wallet,
payment => {
this.checkBalance()
}
)
},
checkBalance: function () {
var self = this
LNbits.api
@@ -307,12 +321,14 @@
this.lnbtc = false
this.onbtc = true
}
this.wallet.inkey = '{{ wallet_inkey }}'
this.getTheTime()
this.getThePercentage()
var timerCount = this.timerCount
if ('{{ charge.paid }}' == 'False') {
timerCount()
}
this.startPaymentNotifier()
}
})
</script>
@@ -475,7 +475,7 @@
},
sendFormDataCharge: function () {
var self = this
var wallet = this.g.user.wallets[0].adminkey
var wallet = this.g.user.wallets[0].inkey
var data = this.formDialogCharge.data
data.amount = parseInt(data.amount)
data.time = parseInt(data.time)
+4 -1
View File
@@ -6,6 +6,7 @@ from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits.core.crud import get_wallet
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
@@ -29,6 +30,8 @@ async def display(request: Request, charge_id):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
)
wallet = await get_wallet(charge.lnbitswallet)
return satspay_renderer().TemplateResponse(
"satspay/display.html", {"request": request, "charge": charge}
"satspay/display.html",
{"request": request, "charge": charge, "wallet_key": wallet.inkey},
)
+16 -8
View File
@@ -5,7 +5,12 @@ from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.extensions.satspay import satspay_ext
from .crud import (
@@ -22,18 +27,21 @@ from .models import CreateCharge
@satspay_ext.post("/api/v1/charge")
async def api_charge_create(
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
):
charge = await create_charge(user=wallet.wallet.user, data=data)
return charge.dict()
@satspay_ext.put("/api/v1/charge/{charge_id}")
async def api_charge_create_or_update(
async def api_charge_update(
data: CreateCharge,
wallet: WalletTypeInfo = Depends(require_admin_key),
charge_id=None,
):
if not charge_id:
charge = await create_charge(user=wallet.wallet.user, data=data)
return charge.dict()
else:
charge = await update_charge(charge_id=charge_id, data=data)
return charge.dict()
charge = await update_charge(charge_id=charge_id, data=data)
return charge.dict()
@satspay_ext.get("/api/v1/charges")
+1 -1
View File
@@ -60,7 +60,7 @@ class Service(BaseModel):
onchain: Optional[str]
servicename: str # Currently, this will just always be "Streamlabs"
authenticated: bool # Whether a token (see below) has been acquired yet
token: Optional[int] # The token with which to authenticate requests
token: Optional[str] # The token with which to authenticate requests
@classmethod
def from_row(cls, row: Row) -> "Service":
@@ -62,7 +62,7 @@
donationDialog: {
show: false,
data: {
name: '',
name: null,
sats: '',
message: ''
}
+6 -4
View File
@@ -7,6 +7,7 @@ from starlette.responses import RedirectResponse
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import (
CreateDonation,
CreateService,
@@ -113,17 +114,18 @@ async def api_create_donation(data: CreateDonation, request: Request):
service_id = data.service
service = await get_service(service_id)
charge_details = await get_charge_details(service.id)
name = data.name
name = data.name if data.name else "Anonymous"
description = f"{sats} sats donation from {name} to {service.twitchuser}"
charge = await create_charge(
create_charge_data = CreateCharge(
amount=sats,
completelink=f"https://twitch.tv/{service.twitchuser}",
completelinktext="Back to Stream!",
webhook=webhook_base + "/streamalerts/api/v1/postdonation",
description=description,
**charge_details,
**charge_details
)
charge = await create_charge(user=charge_details["user"], data=create_charge_data)
await create_donation(
id=charge.id,
wallet=service.wallet,
@@ -173,7 +175,7 @@ async def api_get_donations(g: WalletTypeInfo = Depends(get_key_type)):
for wallet_id in wallet_ids:
new_donations = await get_donations(wallet_id)
donations += new_donations if new_donations else []
return [donation._asdict() for donation in donations] if donations else []
return [donation.dict() for donation in donations] if donations else []
@streamalerts_ext.put("/api/v1/donations/{donation_id}")
+4 -4
View File
@@ -33,7 +33,7 @@ async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomai
async def set_subdomain_paid(payment_hash: str) -> Subdomains:
row = await db.fetchone(
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
(payment_hash,),
)
if row[8] == False:
@@ -66,7 +66,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains:
async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
row = await db.fetchone(
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?",
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.id = ?",
(subdomain_id,),
)
return Subdomains(**row) if row else None
@@ -74,7 +74,7 @@ async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]:
async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]:
row = await db.fetchone(
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.subdomain = ?",
(subdomain,),
)
return Subdomains(**row) if row else None
@@ -86,7 +86,7 @@ async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]:
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN subdomains.domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})",
(*wallet_ids,),
)
+3 -3
View File
@@ -1,5 +1,5 @@
from sqlite3 import Row
from typing import NamedTuple, Optional
from typing import Optional
from fastapi.param_functions import Query
from pydantic import BaseModel
@@ -26,7 +26,7 @@ class createTip(BaseModel):
message: str = ""
class Tip(NamedTuple):
class Tip(BaseModel):
"""A Tip represents a single donation"""
id: str # This ID always corresponds to a satspay charge ID
@@ -55,7 +55,7 @@ class createTips(BaseModel):
message: str
class TipJar(NamedTuple):
class TipJar(BaseModel):
"""A TipJar represents a user's tip jar"""
id: int
+2 -2
View File
@@ -92,7 +92,7 @@ async def api_get_tipjars(wallet: WalletTypeInfo = Depends(get_key_type)):
for wallet_id in wallet_ids:
new_tipjars = await get_tipjars(wallet_id)
tipjars += new_tipjars if new_tipjars else []
return [tipjar._asdict() for tipjar in tipjars] if tipjars else []
return [tipjar.dict() for tipjar in tipjars] if tipjars else []
@tipjar_ext.get("/api/v1/tips")
@@ -103,7 +103,7 @@ async def api_get_tips(wallet: WalletTypeInfo = Depends(get_key_type)):
for wallet_id in wallet_ids:
new_tips = await get_tips(wallet_id)
tips += new_tips if new_tips else []
return [tip._asdict() for tip in tips] if tips else []
return [tip.dict() for tip in tips] if tips else []
@tipjar_ext.put("/api/v1/tips/{tip_id}")
@@ -222,6 +222,7 @@
'INR',
'IQD',
'IRR',
'IRT',
'ISK',
'JEP',
'JMD',
+3 -5
View File
@@ -82,11 +82,9 @@ async def api_wallet_delete(wallet_id, w: WalletTypeInfo = Depends(require_admin
@watchonly_ext.get("/api/v1/address/{wallet_id}")
async def api_fresh_address(wallet_id, w: WalletTypeInfo = Depends(get_key_type)):
await get_fresh_address(wallet_id)
address = await get_fresh_address(wallet_id)
addresses = await get_addresses(wallet_id)
return [address.dict() for address in addresses]
return [address.dict()]
@watchonly_ext.get("/api/v1/addresses/{wallet_id}")
@@ -114,7 +112,7 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
async def api_update_mempool(
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
):
mempool = await update_mempool(endpoint, user=w.wallet.user)
mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user)
return mempool.dict()
@@ -22,7 +22,7 @@
<code>[&lt;withdraw_link_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/links -H "X-Api-Key: {{
>curl -X GET {{ request.base_url }}withdraw/api/v1/links -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
@@ -49,7 +49,7 @@
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -H
>curl -X GET {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
@@ -78,7 +78,7 @@
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/links -d '{"title":
>curl -X POST {{ request.base_url }}withdraw/api/v1/links -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -114,7 +114,7 @@
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -d
>curl -X PUT {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -142,7 +142,7 @@
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt;
>curl -X DELETE {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt;
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
@@ -170,7 +170,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{
}}withdraw/api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
@@ -191,7 +191,7 @@
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}/withdraw/img/&lt;lnurl_id&gt;"
>curl -X GET {{ request.base_url }}withdraw/img/&lt;lnurl_id&gt;"
</code>
</q-card-section>
</q-card>
@@ -7,7 +7,7 @@
{% if link.is_spent %}
<q-badge color="red" class="q-mb-md">Withdraw is spent.</q-badge>
{% endif %}
<a href="lightning://{{ lnurl }}">
<a href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
:value="this.here + '/?lightning={{lnurl }}'"
+5
View File
@@ -71,6 +71,11 @@ async def api_link_create_or_update(
link_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
if data.min_withdrawable < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.max_withdrawable < data.min_withdrawable:
raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
+9
View File
@@ -15,6 +15,7 @@ import lnbits.settings as settings
class Extension(NamedTuple):
code: str
is_valid: bool
is_admin_only: bool
name: Optional[str] = None
short_description: Optional[str] = None
icon: Optional[str] = None
@@ -25,6 +26,7 @@ class Extension(NamedTuple):
class ExtensionManager:
def __init__(self):
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS]
self._extension_folders: List[str] = [
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
][0]
@@ -47,14 +49,17 @@ class ExtensionManager:
) as json_file:
config = json.load(json_file)
is_valid = True
is_admin_only = True if extension in self._admin_only else False
except Exception:
config = {}
is_valid = False
is_admin_only = False
output.append(
Extension(
extension,
is_valid,
is_admin_only,
config.get("name"),
config.get("short_description"),
config.get("icon"),
@@ -155,7 +160,11 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
["lnbits/templates", "lnbits/core/templates", *additional_folders]
)
)
if settings.LNBITS_AD_SPACE:
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
t.env.globals["SITE_DESCRIPTION"] = settings.LNBITS_SITE_DESCRIPTION
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.LNBITS_THEME_OPTIONS
+5
View File
@@ -28,11 +28,16 @@ LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
LNBITS_ALLOWED_USERS: List[str] = env.list(
"LNBITS_ALLOWED_USERS", default=[], subcast=str
)
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
)
LNBITS_AD_SPACE = env.list("LNBITS_AD_SPACE", default=[])
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
LNBITS_SITE_TAGLINE = env.str(
"LNBITS_SITE_TAGLINE", default="free and open-source lightning wallet"
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+37 -11
View File
@@ -111,7 +111,7 @@ window.LNbits = {
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
},
updateWallet: function (walletName, userId, walletId) {
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
},
deleteWallet: function (walletId, userId) {
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
@@ -123,6 +123,7 @@ window.LNbits = {
[
'code',
'isValid',
'isAdminOnly',
'name',
'shortDescription',
'icon',
@@ -135,7 +136,12 @@ window.LNbits = {
return obj
},
user: function (data) {
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
var obj = {
id: data.id,
email: data.email,
extensions: data.extensions,
wallets: data.wallets
}
var mapWallet = this.wallet
obj.wallets = obj.wallets
.map(function (obj) {
@@ -153,16 +159,23 @@ window.LNbits = {
return obj
},
wallet: function (data) {
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
newWallet = {
id: data.id,
name: data.name,
adminkey: data.adminkey,
inkey: data.inkey
}
newWallet.msat = data.balance_msat
newWallet.sat = Math.round(data.balance_msat / 1000)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
newWallet.sat
)
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
return newWallet
},
payment: function (data) {
obj = {
checking_id:data.id,
checking_id: data.id,
pending: data.pending,
amount: data.amount,
fee: data.fee,
@@ -174,8 +187,8 @@ window.LNbits = {
extra: data.extra,
wallet_id: data.wallet_id,
webhook: data.webhook,
webhook_status: data.webhook_status,
}
webhook_status: data.webhook_status
}
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
@@ -225,7 +238,8 @@ window.LNbits = {
Quasar.plugins.Notify.create({
timeout: 5000,
type: types[error.response.status] || 'warning',
message: error.response.data.message || error.response.data.detail || null,
message:
error.response.data.message || error.response.data.detail || null,
caption:
[error.response.status, ' ', error.response.statusText]
.join('')
@@ -331,17 +345,25 @@ window.windowMixin = {
}
},
created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
this.g.allowedThemes = window.allowedThemes ?? ['classic']
if(this.$q.localStorage.getItem('lnbits.darkMode') == true || this.$q.localStorage.getItem('lnbits.darkMode') == false){
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
}
else{
this.$q.dark.set(true)
}
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
// failsafe if admin changes themes halfway
if (!this.$q.localStorage.getItem('lnbits.theme')){
this.changeColor(this.g.allowedThemes[0])
}
if (
this.$q.localStorage.getItem('lnbits.theme') &&
!this.g.allowedThemes.includes(
this.$q.localStorage.getItem('lnbits.theme')
)
) {
console.log('allowedThemes changed by Admin', this.g.allowedThemes[0])
this.changeColor(this.g.allowedThemes[0])
}
@@ -368,6 +390,10 @@ window.windowMixin = {
.filter(function (obj) {
return !obj.hidden
})
.filter(function (obj) {
if (window.user.admin) return obj
return !obj.isAdminOnly
})
.map(function (obj) {
if (user) {
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
+6 -4
View File
@@ -22,7 +22,8 @@ Vue.component('lnbits-wallet-list', {
activeWallet: null,
activeBalance: [],
showForm: false,
walletName: ''
walletName: '',
LNBITS_DENOMINATION: LNBITS_DENOMINATION
}
},
template: `
@@ -43,7 +44,8 @@ Vue.component('lnbits-wallet-list', {
</q-item-section>
<q-item-section>
<q-item-label lines="1">{{ wallet.name }}</q-item-label>
<q-item-label caption>{{ wallet.live_fsat }} sat</q-item-label>
<q-item-label v-if="LNBITS_DENOMINATION != 'sats'" caption>{{ parseFloat(String(wallet.live_fsat).replaceAll(",", "")) / 100 }} {{ LNBITS_DENOMINATION }}</q-item-label>
<q-item-label v-else caption>{{ wallet.live_fsat }} {{ LNBITS_DENOMINATION }}</q-item-label>
</q-item-section>
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
@@ -194,11 +196,11 @@ Vue.component('lnbits-payment-details', {
</div>
<div class="row">
<div class="col-3"><b>Amount</b>:</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} sat</div>
<div class="col-9">{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Fee</b>:</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} sat</div>
<div class="col-9">{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}</div>
</div>
<div class="row">
<div class="col-3"><b>Payment hash</b>:</div>
+8
View File
@@ -7,6 +7,14 @@ $themes: (
marginal-bg: #1f2234,
marginal-text: #fff
),
'bitcoin': (
primary: #ff9853,
secondary: #ff7353,
dark: #2d293b,
info: #333646,
marginal-bg: #2d293b,
marginal-text: #fff
),
'mint': (
primary: #3ab77d,
secondary: #27b065,
+19 -4
View File
@@ -20,7 +20,7 @@
{% block head_scripts %}{% endblock %}
</head>
<body data-theme="classic">
<body data-theme="bitcoin">
<q-layout id="vue" view="hHh lpR lfr" v-cloak>
<q-header bordered class="bg-marginal-bg">
<q-toolbar>
@@ -67,8 +67,19 @@
icon="format_color_fill"
color="deep-purple"
size="md"
><q-tooltip>classic</q-tooltip> </q-btn
><q-btn
><q-tooltip>classic</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeColor('bitcoin')"
icon="format_color_fill"
color="orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('mint')"
dense
flat
@@ -151,11 +162,12 @@
</q-page>
</q-page-container>
{% endblock %} {% block footer %}
<q-footer
class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}"
>
<q-toolbar>
<q-toolbar class="gt-sm">
<q-toolbar-title class="text-caption">
{{ SITE_TITLE }}, {{SITE_TAGLINE}}
<br />
@@ -179,6 +191,7 @@
</q-btn>
</q-toolbar>
</q-footer>
{% endblock %}
</q-layout>
@@ -192,6 +205,8 @@
<script src="/static/js/components.js"></script>
<script type="text/javascript">
const themes = {{ LNBITS_THEME_OPTIONS | tojson }}
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson}}
console.log(LNBITS_DENOMINATION)
if(themes && themes.length) {
window.allowedThemes = themes.map(str => str.trim())
}
+12 -1
View File
@@ -71,6 +71,7 @@ currencies = {
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
"IRT": "Iranian Toman",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
exchange_rate_providers = {
"exir": Provider(
"Exir",
"exir.io",
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
lambda data, replacements: data["last"],
),
"bitfinex": Provider(
"Bitfinex",
"bitfinex.com",
@@ -279,8 +286,12 @@ async def btc_price(currency: str) -> float:
async def get_fiat_rate_satoshis(currency: str) -> float:
return int(100_000_000 / (await btc_price(currency)))
return float(100_000_000 / (await btc_price(currency)))
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
return int(amount * (await get_fiat_rate_satoshis(currency)))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
return float(amount / (await get_fiat_rate_satoshis(currency)))
+1 -1
View File
@@ -2,10 +2,10 @@
from .void import VoidWallet
from .clightning import CLightningWallet
from .lndgrpc import LndWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet
from .lnbits import LNbitsWallet
from .lndrest import LndRestWallet
from .spark import SparkWallet
from .fake import FakeWallet
+3 -1
View File
@@ -60,7 +60,9 @@ class Wallet(ABC):
pass
@abstractmethod
def pay_invoice(self, bolt11: str) -> Coroutine[None, None, PaymentResponse]:
def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> Coroutine[None, None, PaymentResponse]:
pass
@abstractmethod
+13 -4
View File
@@ -18,6 +18,7 @@ from .base import (
Unsupported,
Wallet,
)
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func):
@@ -31,8 +32,8 @@ def async_wrap(func):
return run
def _pay_invoice(ln, bolt11):
return ln.pay(bolt11)
def _pay_invoice(ln, payload):
return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index):
@@ -102,10 +103,18 @@ class CLightningWallet(Wallet):
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = lnbits_bolt11.decode(bolt11)
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
payload = {
"bolt11": bolt11,
"maxfeepercent": "{:.11}".format(fee_limit_percent),
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee under 5000 millisatoshi (which is default value of exemptfee)
}
try:
wrapped = async_wrap(_pay_invoice)
r = await wrapped(self.ln, bolt11)
r = await wrapped(self.ln, payload)
except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc))

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