Compare commits

..
978 Commits
Author SHA1 Message Date
ArcandGitHub 2d41f0c4ab Merge pull request #469 from lnbits/StepansFix
Makes lnurlpos work with latest lnurlpos.ino
2021-12-21 17:15:19 +00:00
benarc 0bb5d6769e removed currency 2021-12-21 17:14:17 +00:00
benarc d517fe0330 chnaged req to request 2021-12-21 16:20:53 +00:00
benarc ee4f93d3ba removed s read check 2021-12-21 15:55:24 +00:00
benarc 1e171e660d removed currency check 2021-12-21 15:53:40 +00:00
benarc b04eea6463 encode key as byte 2021-12-21 15:47:39 +00:00
benarc 0b93811957 added key 2021-12-21 15:38:19 +00:00
benarc c87abef20f Added Stepans decode functions 2021-12-21 15:33:52 +00:00
ArcandGitHub 6b1c1af148 Merge pull request #467 from arcbtc/FastAPI
benched lnurlpayout
2021-12-19 23:26:54 +00:00
benarc 31c131e2ab benched lnurlpayout 2021-12-19 23:25:13 +00:00
benarc a86dcb9586 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-12-19 23:24:12 +00:00
ArcandGitHub 6b86506a64 Merge pull request #455 from arcbtc/lnurlpayout
Adds LNURLPayout: auto-payout to an LNURL
2021-12-19 23:15:07 +00:00
benarc 4408f30285 removed print 2021-12-19 23:13:15 +00:00
benarc 31beec4f48 All works 2021-12-19 22:58:23 +00:00
benarc 6a4290c0e9 paying function works 2021-12-19 22:28:19 +00:00
Charles Hillandfiatjaf 7cefd200aa lntxbot.bigsun.xyz will expire soon, use lntxbot.com instead
See: https://twitter.com/lntxbot/status/1457341136918482945
2021-12-17 19:55:31 -03:00
benarc 331a383223 Adding manual check 2021-12-17 12:15:33 +00:00
benarc 22440492d9 adding api point for manual checking lnurlpayouts 2021-12-16 23:55:10 +00:00
benarc 7cbf8d8902 Added title 2021-12-16 23:39:51 +00:00
benarc 55ec6b7fbe Merge remote-tracking branch 'origin/FastAPI' into lnurlpayout 2021-12-16 21:18:38 +00:00
fiatjaf 703e6107fa remove old fiatjaf's checksum thing (replaced with stepan's). 2021-12-16 15:55:36 -03:00
ArcandGitHub 207961307a Merge pull request #465 from arcbtc/FastAPI
changed server to base URL
2021-12-16 17:14:59 +00:00
Ben Arc 13a34d78e2 server to baseURL 2021-12-16 17:07:23 +00:00
ArcandGitHub d0e437d1dc Merge pull request #464 from arcbtc/Stepansfix
Adds stepans fix
2021-12-16 17:06:36 +00:00
Ben Arc cee82abb54 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-12-16 17:06:11 +00:00
Ben Arc b570d82a68 Formatted 2021-12-16 17:01:45 +00:00
benarc 4f6e49b02f added Stepans fix 2021-12-15 14:47:45 +00:00
benarc b968a0c13f Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-12-15 13:00:06 +00:00
ArcandGitHub 2609c2124e Merge pull request #463 from lnbits/compatible-lnurlpos
lnurlpos v2 lnurl endpoint (backwards-compatible)
2021-12-15 00:21:27 +00:00
Ben Arc bb1b452134 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-12-15 00:18:24 +00:00
fiatjaf 09990450b7 lnurlpos v2 lnurl endpoint.
compatible with the phone app and updated device.
2021-12-14 17:00:20 -03:00
benarc f3876d547e Added title 2021-12-09 18:04:04 +00:00
ArcandGitHub a2bb340601 Merge pull request #460 from arcbtc/FastAPI
lnurlp display without refresh
2021-12-09 10:34:34 +00:00
benarc d108a70ec8 Merge branch 'FastAPI' into origin/FastAPI 2021-12-09 10:32:31 +00:00
benarc d00bd9a995 lnul display without refresh 2021-12-09 10:32:03 +00:00
benarc 63d68d60ce Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-12-09 10:17:26 +00:00
benarc 1227fb980f few prints 2021-12-09 10:16:40 +00:00
calleandGitHub 43b48376bb lndrest fees for fastapi (#459) 2021-12-09 00:26:08 +01:00
benarc 7e6b2ba029 Should work, listener logic in 2021-12-08 11:43:13 +00:00
benarc 0ab158992e Checks wallet balance is at threshold 2021-12-08 10:28:43 +00:00
benarc 410689e04b completing listener, need to fetch pay lnurlpay 2021-12-06 13:07:47 +00:00
benarc 5f3bbd9695 Should be working 2021-12-06 12:56:48 +00:00
ArcandGitHub d21b68a5e7 Merge pull request #417 from chill117/fastapi-tests
Unit tests for FastAPI branch
2021-12-05 22:13:10 +00:00
ArcandGitHub 02fa0877c0 Merge pull request #409 from chill117/fix-dockerfile
Fix Dockerfile for FastAPI branch
2021-12-05 20:48:53 +00:00
Charles Hill 4e6c30a909 Unit tests for FastAPI branch
Run via `make test`
2021-12-04 13:11:45 -06:00
benarc 9465347669 Added listener, need to check wallet id agianst lnurlpayouts 2021-12-03 10:50:17 +00:00
benarc 1acdd4b548 Added restrictions 2021-12-03 00:36:02 +00:00
benarc fa23be7657 Added check fixed LNURL decode 2021-12-03 00:17:32 +00:00
benarc 2490b218b1 Merge remote-tracking branch 'origin/FastAPI' into lnurlpayout 2021-12-02 22:56:54 +00:00
benarc cacee8d072 Form works, check LNURL is valid url 2021-12-02 22:56:31 +00:00
ArcandGitHub 98c3420aba Merge pull request #448 from arcbtc/FastAPI
LNaddress updates and fee reserve check
2021-12-02 21:24:10 +00:00
benarc 907714838b Added check to explain fee reserve 2021-12-02 21:22:30 +00:00
benarc ccc52dc585 Merge branch 'FastAPI' into lnurlpayout 2021-12-02 11:47:00 +00:00
benarc 0233aa51b1 Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-12-01 21:55:47 +00:00
benarc 69a4583203 wos debugging
lnurl debug

text identifier

text

try1

try2

debugging

more debug

...

....

hard code indentifier

.

more hard code

.

trying

revert

remove identifier

adding identifier...again

.

stupid

return text instead of json

htmlresponse

on more spin

.

added tag

revert to JSON return

.

ditch lnurl lib

.

..

..

clean prints
2021-12-01 21:55:04 +00:00
Tiago vasconcelos d1a9960acb clean prints 2021-12-01 19:41:17 +00:00
Tiago vasconcelos 2302937fc9 .. 2021-12-01 12:28:42 +00:00
Tiago vasconcelos 4790bf21b2 .. 2021-12-01 12:27:47 +00:00
Tiago vasconcelos 35f24b828d . 2021-12-01 12:25:51 +00:00
Tiago vasconcelos 7b8c9cfdc5 ditch lnurl lib 2021-12-01 12:19:55 +00:00
Tiago vasconcelos 9cc3aaa7a2 . 2021-12-01 11:50:45 +00:00
Tiago vasconcelos 3fd519e9dc revert to JSON return 2021-12-01 11:49:04 +00:00
Tiago vasconcelos 55cb78710b added tag 2021-12-01 11:40:03 +00:00
Tiago vasconcelos b6498389bc . 2021-12-01 11:33:38 +00:00
Tiago vasconcelos c24d28bff4 on more spin 2021-12-01 11:31:47 +00:00
Tiago vasconcelos 8cdbd02a77 htmlresponse 2021-12-01 11:27:43 +00:00
Tiago vasconcelos 2a9a0bd7aa return text instead of json 2021-12-01 11:21:54 +00:00
Tiago vasconcelos 205c53d8e9 stupid 2021-12-01 11:11:51 +00:00
Tiago vasconcelos 346e50cbb5 . 2021-12-01 11:10:19 +00:00
Tiago vasconcelos e6ec560317 adding identifier...again 2021-12-01 11:06:56 +00:00
Tiago vasconcelos a7ceae86b4 remove identifier 2021-12-01 10:58:04 +00:00
Tiago vasconcelos 3e6a5b6720 revert 2021-12-01 10:47:39 +00:00
Tiago vasconcelos 177cdecabf trying 2021-12-01 10:44:54 +00:00
Tiago vasconcelos 395613040b . 2021-12-01 10:39:12 +00:00
Tiago vasconcelos e21f850ef3 more hard code 2021-12-01 10:37:40 +00:00
Tiago vasconcelos f1394f3697 . 2021-12-01 10:33:37 +00:00
Tiago vasconcelos 35eb8cd313 hard code indentifier 2021-12-01 10:32:37 +00:00
Tiago vasconcelos 860c0258c2 .... 2021-12-01 10:25:21 +00:00
Tiago vasconcelos 16b6ae461a ... 2021-12-01 10:23:54 +00:00
Tiago vasconcelos d89d80c245 more debug 2021-12-01 10:19:59 +00:00
Tiago vasconcelos 52503c2ba9 debugging 2021-12-01 10:07:34 +00:00
Tiago vasconcelos 4411780f82 try2 2021-12-01 09:56:42 +00:00
Tiago vasconcelos 44e0dd4c92 try1 2021-12-01 09:53:00 +00:00
Tiago vasconcelos a628f125c9 text 2021-12-01 09:46:04 +00:00
Tiago vasconcelos 4ee86958b8 text identifier 2021-12-01 09:44:19 +00:00
Tiago vasconcelos de2ebf90c7 lnurl debug 2021-11-30 21:41:38 +00:00
Tiago vasconcelos c5d7b7f411 wos debugging 2021-11-30 21:19:05 +00:00
ArcandGitHub 152d0c3c41 Merge pull request #446 from arcbtc/FastAPI
Fixes lnaddress
2021-11-30 16:15:00 +00:00
Tiago vasconcelos f7eba156e7 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-11-30 15:13:56 +00:00
Tiago vasconcelos 290268885d fix purge
check for missing protocol

fix request url default

more debub

fix lnbits root url

add documentation reference
2021-11-30 15:13:06 +00:00
Tiago vasconcelos e3360514cd add documentation reference 2021-11-30 15:12:13 +00:00
Tiago vasconcelos ccd3a12ed3 fix lnbits root url 2021-11-30 14:58:03 +00:00
Tiago vasconcelos d6f3c2f9c4 more debub 2021-11-30 14:46:48 +00:00
Tiago vasconcelos f37af0ddf6 fix request url default 2021-11-30 14:41:25 +00:00
Tiago vasconcelos 5b01ee6879 check for missing protocol 2021-11-30 14:20:34 +00:00
Tiago vasconcelos 63546036bc fix purge 2021-11-30 14:12:01 +00:00
benarc b9bc36fabf Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-11-30 10:08:46 +00:00
ArcandGitHub b4ad8cb5b2 Merge pull request #443 from lnbits/fastapi_fix_lndrestwallet_fee_limit
(FastAPI) LNDRest fee limit
2021-11-30 09:04:01 +00:00
callebtc 12c2a04b2a LNDRest fee limit 2021-11-29 18:10:11 +01:00
benarc b7fcf461f1 init 2021-11-28 18:11:35 +00:00
ArcandGitHub 1609280f53 Merge pull request #438 from lnbits/usermanager_api_fix
Fix UserManager endpoint collision (FastAPI)
2021-11-28 17:04:48 +00:00
benarc e3c6485903 Merge remote-tracking branch 'fusion44/FastAPI' into FastAPI 2021-11-28 17:01:16 +00:00
callebtc b178d9a38d readd mypy.ini 2021-11-27 13:11:29 +01:00
callebtc ad3942f844 remove type comment 2021-11-27 13:01:12 +01:00
callebtc 9b58bfe06a remove trio_typing 2021-11-27 12:54:01 +01:00
callebtc 2612e60647 annotation for optional attributes 2021-11-27 12:43:14 +01:00
callebtc 3fd66f38da rename endpoint to avoid collision 2021-11-27 12:42:19 +01:00
Stefan Stammberger 3831bca9e2 fix: cleanup core/views/generic.py
* Fixed withdraw endpoint query params access
    was: request.query_params.get("usr")
    corrected: request.query_params.get("usr")
* Added missing response classes
* Removed old comments and commeted out code
* Deleted the unused auth_bearer.py file
2021-11-27 10:13:32 +01:00
ArcandGitHub ed198d3e7d Merge pull request #435 from arcbtc/FastAPI
Cleanup and black
2021-11-26 05:59:10 +00:00
benarc 56397026c8 Chore, applied black 2021-11-26 05:58:20 +00:00
Stefan Stammbergerandbenarc efec7cb8f0 Revert "chore: apply black to all .py files"
This reverts commit 83b7779972.
2021-11-26 05:56:30 +00:00
Tiago vasconcelos cfac70d394 general cleanup and unused imports removal 2021-11-25 18:52:16 +00:00
Tiago vasconcelos 2b0bd43974 clean prints 2021-11-25 17:26:46 +00:00
Tiago vasconcelos b33f0ecae2 request issues again 2021-11-25 17:24:03 +00:00
Tiago vasconcelos ae50b909eb again 2021-11-25 17:19:49 +00:00
Tiago vasconcelos 98a55bf0df request client 2021-11-25 17:12:41 +00:00
Tiago vasconcelos 4fff5c3b7a missing request 2021-11-25 17:08:20 +00:00
Tiago vasconcelos 1b29eb9f75 fix lnaddress 2021-11-25 16:24:11 +00:00
Stefan Stammberger 83b7779972 chore: apply black to all .py files 2021-11-24 14:31:55 +01:00
ArcandGitHub d93aecf960 Merge pull request #433 from arcbtc/FastAPI
fixed lnaddress for testing
2021-11-24 11:22:25 +00:00
benarc 3b80ff5171 disabled lnaddress 2021-11-24 11:20:56 +00:00
Tiago vasconcelos c441c765b2 fix lnaddress not showing 2021-11-24 10:52:34 +00:00
ArcandGitHub c8e2aba451 Merge pull request #432 from arcbtc/FastAPI
added lnaddresss for testing
2021-11-24 10:11:37 +00:00
benarc e44b5051b5 added 10 min check 2021-11-24 10:11:00 +00:00
benarc 5cb392e5dc changed min to 10 2021-11-24 10:10:02 +00:00
ArcandGitHub 66c192d6f2 Merge pull request #431 from arcbtc/FastAPI
added lnaddresss for testing
2021-11-24 10:03:41 +00:00
benarc 947d169baa refresh paylinks after create 2021-11-24 10:02:56 +00:00
ArcandGitHub b9f3f35718 Merge pull request #430 from arcbtc/FastAPI
added lnaddresss for testing
2021-11-24 09:57:05 +00:00
benarc f1dea9fc9b weighted chance to account for transaction fees 2021-11-24 09:56:24 +00:00
ArcandGitHub 09ccf01fbb Merge pull request #429 from arcbtc/FastAPI
added lnaddresss for testing
2021-11-24 08:56:13 +00:00
benarc 246942509c added lnaddress for testing 2021-11-24 08:54:28 +00:00
benarc b122debd8c Merge remote-tracking branch 'arcbtc/lnaddress' into FastAPI 2021-11-24 08:42:14 +00:00
ArcandGitHub 74eb496d67 Merge pull request #428 from arcbtc/FastAPI
merge
2021-11-23 11:04:28 +00:00
benarc bc0b64a3fb Fixed lnurlpos links not showing 2021-11-23 11:03:57 +00:00
ArcandGitHub e2d1e3d261 Merge pull request #427 from arcbtc/FastAPI
merge
2021-11-23 10:28:13 +00:00
Tiago vasconcelos 1406949ceb added postgres convertion and updated guide 2021-11-18 11:33:26 +00:00
Tiago vasconcelos 2676e136cb Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-11-17 21:20:53 +00:00
Tiago vasconcelos 3f8d1a6fb5 fix withdraw routes order 2021-11-17 21:20:48 +00:00
Stefan Stammberger d8e4237961 fix: TipJar wallet access bug 2021-11-17 14:33:23 +01:00
Tiago vasconcelos 2485e8eabf fix url_for no allowing query params 2021-11-17 12:56:43 +00:00
Charles Hillandfiatjaf 4c77d6f11e Fix Bleskomat extension for FastAPI branch 2021-11-16 11:59:34 -03:00
ArcandGitHub 1cd7823998 Merge pull request #423 from arcbtc/FastAPI
Posgres support
2021-11-15 21:31:05 -06:00
benarc 1ee50006e8 fixed bug 2021-11-16 03:30:37 +00:00
ArcandGitHub 6da1aec2b9 Merge pull request #422 from arcbtc/FastAPI
Posgres support
2021-11-15 21:26:34 -06:00
benarc a58924184e Fixed bug 2021-11-16 03:25:13 +00:00
ArcandGitHub 05299224ee Merge pull request #421 from arcbtc/FastAPI
Posgres support
2021-11-15 21:19:12 -06:00
benarc da43923d2f fixed bug 2021-11-16 03:18:06 +00:00
ArcandGitHub 7d0598f5cf Merge pull request #420 from arcbtc/FastAPI
Posgres support
2021-11-15 21:15:40 -06:00
ArcandGitHub feb5a9e6c8 Update views.py 2021-11-16 03:11:01 +00:00
benarc 99d113f7d1 Fixed lost bug 2021-11-16 03:08:51 +00:00
ArcandGitHub 5ccf5111a5 Merge pull request #419 from arcbtc/FastAPI
Posgres support
2021-11-15 20:53:40 -06:00
benarc 03ed185aac Fixed lost bug 2021-11-16 02:53:16 +00:00
ArcandGitHub a3d2e43a4c Merge pull request #418 from arcbtc/FastAPI
Posgres support
2021-11-15 20:46:49 -06:00
Tiago vasconcelos e8882645c4 remove IF NOT EXISTS in CREATE VIEW 2021-11-15 21:45:13 +00:00
Tiago vasconcelos c34041c49c add hack for milliseconds on postgres 2021-11-15 12:11:42 +00:00
calleandGitHub e46e881663 Merge pull request #416 from lnbits/fix_db_lock_fastapi
Fix db connect
2021-11-13 12:51:19 +01:00
callebtc bec0b1d1fe fix db connect 2021-11-13 11:42:11 +01:00
fiatjaf 0331861cc6 prevent pay_invoice from locking sqlite for the entire app. 2021-11-12 13:05:10 -03:00
Tiago vasconcelos c48ae1c1c9 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-11-12 11:48:33 +00:00
Tiago vasconcelos 24cbbc0cd5 fix psql typo 2021-11-12 11:44:44 +00:00
Tiago vasconcelos 7ad75cb1a4 update install guide to postgres 2021-11-12 11:38:07 +00:00
benarc 78f7a3914f Fixed error reporting for lntxbot source 2021-11-12 10:16:15 +00:00
Charles Hill f1c068a115 Fix Dockerfile 2021-11-12 02:02:13 -05:00
ArcandGitHub e9b9197641 Merge pull request #407 from arcbtc/FastAPI
black
2021-11-12 04:15:27 +00:00
benarc 267dea4f75 black 2021-11-12 04:14:55 +00:00
ArcandGitHub 11ce32cf04 Merge pull request #406 from arcbtc/FastAPI
Added some IF EXISTS
2021-11-10 21:18:56 +00:00
Ben Arc 860709f48c async wrap 2021-11-10 21:14:47 +00:00
Ben Arc e0bfb15e42 Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-11-10 21:13:08 +00:00
Ben Arc 119404b535 updated withdraw lnurl 2021-11-10 21:12:47 +00:00
Tiago vasconcelos 1537b06e00 reset form 2021-11-10 17:00:53 +00:00
Tiago vasconcelos 10fb16467b url_for not working 2021-11-10 11:15:33 +00:00
Ben Arc 2caa4f2df4 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-11-09 23:59:30 +00:00
ArcandGitHub 2f9751940e Update installation.md 2021-11-09 22:45:45 +00:00
ArcandGitHub 6b6d300058 Update installation.md 2021-11-09 22:44:11 +00:00
ArcandGitHub d81559ba2e Update installation.md 2021-11-09 22:41:24 +00:00
ArcandGitHub 3b8920b436 Update installation.md 2021-11-09 22:40:11 +00:00
ArcandGitHub 17db309f35 Update installation.md 2021-11-09 22:39:20 +00:00
ArcandGitHub 1c06781617 Merge pull request #405 from arcbtc/FastAPI
Added some IF EXISTS
2021-11-09 22:29:27 +00:00
Ben Arc cbc24a8fa5 Changed optional unit to sat 2021-11-09 22:25:20 +00:00
ArcandGitHub 1f4d5f4380 Merge pull request #404 from arcbtc/FastAPI
Added some IF EXISTS
2021-11-09 20:54:20 +00:00
Ben Arc 636da1eefd Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-11-09 20:50:07 +00:00
Ben Arc 1b46b61da9 changed see endpoint key back 2021-11-09 20:50:00 +00:00
Stefan Stammberger cba8fe40bd Merge branch 'FastAPI' of github.com:arcbtc/lnbits into FastAPI 2021-11-09 19:16:02 +01:00
Stefan Stammberger 0968c1b182 feat: print a message when all migrations are done 2021-11-09 19:15:43 +01:00
ArcandGitHub 43060ca0e8 Merge pull request #403 from arcbtc/FastAPI
Added some IF EXISTS
2021-11-09 18:09:18 +00:00
Ben Arc 196cdf7737 Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-11-09 17:44:15 +00:00
Ben Arc 81d8d4e218 plugged few key issues 2021-11-09 17:44:05 +00:00
Stefan Stammberger 8ea2d20389 fix: wallets not loading when protobuf is missing 2021-11-09 18:15:07 +01:00
ArcandGitHub 3611c9f7c4 Merge pull request #402 from arcbtc/FastAPI
Added some IF EXISTS
2021-11-09 15:23:17 +00:00
Ben Arc a50c7b6e3a Added some IF NOT EXISTS to avoid errors on startup 2021-11-09 15:17:44 +00:00
Tiago vasconcelos 2c4c7369f3 fix currencies list 2021-11-09 10:41:59 +00:00
Tiago vasconcelos 084ecb1c08 fix missing number on model 2021-11-08 15:55:23 +00:00
Tiago vasconcelos 68dd95dc59 fix awaits 2021-11-08 12:28:11 +00:00
Tiago vasconcelos df0b38f346 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-11-08 12:17:40 +00:00
ArcandGitHub e721ae1d12 Update installation.md 2021-11-08 09:50:11 +00:00
ArcandGitHub 5eae8e1b11 Merge pull request #400 from arcbtc/FastAPI
fix: switch from purerpc to grpcio for LND Wallet
2021-11-07 18:48:22 +00:00
Stefan Stammberger 768aa05b11 fix: switch from purerpc to grpcio for LND Wallet
purerpc is unmaintained and had some old dependencies.
2021-11-07 17:24:22 +01:00
ArcandGitHub d1944e919c Merge pull request #398 from arcbtc/FastAPI
cln sse should be working
2021-11-04 21:08:57 +00:00
Stefan Stammberger 54f4dfd838 fix: paid_invoices_stream for C-Lightning wallet 2021-11-04 19:49:48 +01:00
Tiago vasconcelos 1718e3d804 fix satsdice update 2021-11-04 16:41:23 +00:00
Tiago vasconcelos f220797a35 keep port 5000 2021-11-04 15:21:17 +00:00
Tiago vasconcelos ee34d978d8 install guide fastapi 2021-11-04 15:12:10 +00:00
ArcandGitHub 16927031df Merge pull request #397 from arcbtc/FastAPI
cln sse should be working
2021-11-04 12:57:56 +00:00
benarc 114a6f1fed Made memo optional in model 2021-11-04 12:57:28 +00:00
ArcandGitHub 1ce5940e3a Merge pull request #396 from arcbtc/FastAPI
cln sse should be working
2021-11-04 12:46:48 +00:00
benarc 730c0e0b3d patched bug 2021-11-04 12:46:22 +00:00
ArcandGitHub 77252ed376 Merge pull request #395 from arcbtc/FastAPI
cln sse should be working
2021-11-04 12:41:59 +00:00
benarc 9b1cadaffc Cleaned up lnurl-withdraw 2021-11-04 12:41:31 +00:00
ArcandGitHub 6da194ce6e Merge pull request #394 from arcbtc/FastAPI
cln sse should be working
2021-11-04 11:13:59 +00:00
benarc 1ac84f1445 fixed satdice 2021-11-04 11:11:56 +00:00
ArcandGitHub c063e245dc Merge pull request #393 from arcbtc/FastAPI
cln sse should be working
2021-11-03 22:56:16 +00:00
benarc 2eed8b620c removed header 2021-11-03 22:55:44 +00:00
ArcandGitHub a823f450b2 Merge pull request #392 from arcbtc/FastAPI
cln sse should be working
2021-11-03 22:36:13 +00:00
benarc a4ca6910fa tidied spark 2021-11-03 22:35:01 +00:00
ArcandGitHub 0ca033e1a4 Merge pull request #391 from arcbtc/FastAPI
cln sse should be working
2021-11-03 19:15:30 +00:00
benarc 7af0b786d1 cln sse should be working 2021-11-03 19:15:02 +00:00
ArcandGitHub d786047c4a Merge pull request #390 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 16:15:18 +00:00
benarc 92ea0635d7 another cln rpc sse try 2021-11-03 16:14:47 +00:00
ArcandGitHub a954232049 Merge pull request #389 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 16:07:42 +00:00
benarc a486cbe278 broken cln sse rpc 2021-11-03 16:07:04 +00:00
ArcandGitHub 7c06991d94 Merge pull request #388 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 15:24:48 +00:00
benarc 13bec714d5 lighningd payment stream not working 2021-11-03 15:24:24 +00:00
ArcandGitHub 9d7bcce515 Merge pull request #387 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 14:42:01 +00:00
benarc c1d6d3eee6 lnurlpos working 2021-11-03 14:41:36 +00:00
ArcandGitHub f958b00009 Merge pull request #386 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 12:44:57 +00:00
benarc e62f2e2c34 added request to lnurlpos callback 2021-11-03 12:44:32 +00:00
ArcandGitHub 502782334d Merge pull request #385 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 12:34:35 +00:00
benarc 980a878537 added manual payment check for offline shop 2021-11-03 12:33:29 +00:00
ArcandGitHub 5662f3ac0d Merge pull request #384 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 12:10:03 +00:00
benarc 43e016633b fixed unit check 2021-11-03 12:09:39 +00:00
ArcandGitHub 4e4bd376cf Merge pull request #383 from arcbtc/FastAPI
fixed descrhash check
2021-11-03 12:04:14 +00:00
benarc 3750dcfd0e fixed descrhash check 2021-11-03 12:03:48 +00:00
ArcandGitHub d53a680c2d Merge pull request #382 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:49:53 +00:00
benarc afbe46be8b reverted memo default 2021-11-03 11:49:13 +00:00
ArcandGitHub a37350983a Merge pull request #381 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:23:43 +00:00
benarc 0c0e6e9275 chnaged memo back from optional 2021-11-03 11:23:16 +00:00
ArcandGitHub 4e6c9548ee Merge pull request #380 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:20:37 +00:00
benarc 4fe45b34d1 added default memo value 2021-11-03 11:19:51 +00:00
ArcandGitHub 12dcf188de Merge pull request #379 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:15:15 +00:00
benarc 3336767057 made default meme 2021-11-03 11:14:41 +00:00
ArcandGitHub 6c2ad20a92 Merge pull request #378 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:07:28 +00:00
benarc 97e4ced2f4 fixed bug 2021-11-03 11:07:01 +00:00
ArcandGitHub 828238e318 Merge pull request #377 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 11:03:36 +00:00
benarc 1e92b863d4 Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-11-03 11:02:48 +00:00
benarc 52455e14cd tweaked create_invoice 2021-11-03 11:02:33 +00:00
Tiago vasconcelos 730f673e34 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-11-03 10:30:48 +00:00
Tiago vasconcelos d4d3ef2594 lndhub model required fields 2021-11-03 10:30:43 +00:00
ArcandGitHub cfdf3a2008 Merge pull request #376 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 10:30:05 +00:00
benarc abff990ec3 Changed lnbits wallet check to detail from message 2021-11-03 10:29:39 +00:00
ArcandGitHub 4d3d4a520f Merge pull request #375 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 10:12:43 +00:00
benarc c734526bc1 reverted exception 2021-11-03 10:12:14 +00:00
ArcandGitHub e7e8d307a0 Merge pull request #374 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 10:02:48 +00:00
benarc 573ad3fb73 Added a try to fix lnbits funding source errors 2021-11-03 10:02:21 +00:00
ArcandGitHub b4ed58d4e9 Merge pull request #373 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-03 09:51:34 +00:00
benarc 00ec6f2af3 added NOT_FOUND to check payment endpoint, rather than OK 2021-11-03 09:49:52 +00:00
ArcandGitHub a66090ff1b Merge pull request #372 from arcbtc/FastAPI
restored create invoice api endpoint
2021-11-02 16:38:29 +00:00
benarc 9630d8b363 restored create invoice api endpoint 2021-11-02 16:29:15 +00:00
ArcandGitHub 1fb2fee99b Merge pull request #369 from arcbtc/FastAPI
latest fastapi chnages
2021-10-29 18:23:51 +01:00
Tiago vasconcelos aff29663af lnaddress_ext proposal untested 2021-10-29 16:43:26 +01:00
Tiago vasconcelos a276764f12 wallet fiat conversion fix 2021-10-29 13:06:59 +01:00
Tiago vasconcelos ea99fcb203 streamalert fix request scheme 2021-10-29 12:50:43 +01:00
Tiago vasconcelos cfe7fc5e58 streamalerts converted untested 2021-10-29 12:44:45 +01:00
Tiago vasconcelos 6278e5357c livestreams converted 2021-10-28 17:02:07 +01:00
Tiago vasconcelos f15b6b128b clean up 2021-10-28 17:01:49 +01:00
Tiago vasconcelos ff1e8aff10 hivemind converted 2021-10-28 10:55:22 +01:00
Tiago vasconcelos 20329c9778 fix not fetching domains 2021-10-27 22:17:27 +01:00
Tiago vasconcelos cf44dc0841 subdomains untested convert 2021-10-27 22:09:55 +01:00
Tiago vasconcelos 1021d3c81c paywall converted but create invoice creates BAD REQUEST 2021-10-27 12:02:27 +01:00
Tiago vasconcelos 1653c45f12 fix bleskomat 2021-10-27 12:01:45 +01:00
Tiago vasconcelos 336e3a833a paywall initial 2021-10-26 22:52:11 +01:00
Tiago vasconcelos 92ba2db276 bleskomat with import error in views_api 2021-10-26 22:50:29 +01:00
Tiago vasconcelos 4653e81695 blesko initial commit 2021-10-26 16:48:04 +01:00
Tiago vasconcelos 55023fa85b fix delete wallet 2021-10-25 19:26:21 +01:00
Tiago vasconcelos 8bf14ca2c7 Revert "proposal to allow decimal valued items in fiat"
This reverts commit e58a6923fc.
2021-10-25 18:43:51 +01:00
Tiago vasconcelos 0125c07c6b Revert "proposal to allow decimal valued items in fiat"
This reverts commit e58a6923fc.
2021-10-25 18:42:36 +01:00
Tiago vasconcelos ee30950b97 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-10-25 18:39:11 +01:00
Tiago vasconcelos e58a6923fc proposal to allow decimal valued items in fiat 2021-10-25 18:25:57 +01:00
Stefan Stammberger fa1f6e5038 fix: crash when a currency pair is unavailable 2021-10-25 18:57:58 +02:00
Tiago vasconcelos c4a63e0f2e fix create aditional wallet 2021-10-25 12:42:24 +01:00
Tiago vasconcelos a9e9c67a6f fix vue console error getservices 2021-10-25 12:19:02 +01:00
Tiago vasconcelos 11499041be fix lnurlp update link 2021-10-25 12:11:58 +01:00
Tiago vasconcelos a626155c33 satsdice still errors on withdraw 2021-10-25 11:54:58 +01:00
Stefan Stammberger d1b628375a fix: revert some changes from last commit
Reintroduced the controller which canceles remaining requests once
two results are received.
2021-10-24 10:14:16 +02:00
Stefan Stammberger 7550c968e7 fix: fetch btc price function 2021-10-23 16:41:53 +02:00
Tiago vasconcelos e532ce1971 dirty fix for exchange rates 2021-10-22 11:00:15 +01:00
benarc a6de60aab7 Fixed exchagerates. I think 2021-10-22 09:41:06 +01:00
benarc 7be1859c94 print 2021-10-22 09:37:15 +01:00
ArcandGitHub 7102571c3c Merge pull request #366 from arcbtc/FastAPI
Some payment fixes
2021-10-22 09:25:23 +01:00
benarc 29ae4ef77e added payhash to curl 2021-10-22 09:24:48 +01:00
ArcandGitHub 537a243989 Merge pull request #365 from arcbtc/FastAPI
Some payment fixes
2021-10-22 09:22:00 +01:00
benarc 0a6183a2ec Trying to fix payhash error 2021-10-22 09:21:20 +01:00
ArcandGitHub d05ba94800 Merge pull request #364 from arcbtc/FastAPI
Some payment fixes
2021-10-22 09:12:52 +01:00
benarc 6bcf409ed6 added path name to lnurlpos 2021-10-22 09:11:48 +01:00
benarc f58775a78f No idea why satsdice isnt working 2021-10-22 03:05:48 +01:00
ArcandGitHub 5eff7ff071 Merge pull request #363 from arcbtc/FastAPI
Some payment fixes
2021-10-22 00:43:32 +01:00
benarc fd79815c83 deactivated ngrok 2021-10-22 00:42:26 +01:00
benarc e03370188a Fixed payment checking 2021-10-22 00:41:30 +01:00
Tiago vasconcelos 6c7248ae94 fix form update 2021-10-21 15:53:02 +01:00
Tiago vasconcelos 653f9a9182 fix update args 2021-10-21 11:47:21 +01:00
ArcandGitHub 0a16e15fb8 Merge pull request #362 from arcbtc/FastAPI
ready for testing branch
2021-10-21 11:05:33 +01:00
benarc c1f849b37a tipjar working 2021-10-21 10:42:38 +01:00
ArcandGitHub 055e91dadc Merge pull request #361 from arcbtc/FastAPI
ready for testing branch
2021-10-20 18:46:23 +01:00
benarc 0be429b539 ngrok disabled 2021-10-20 18:45:22 +01:00
benarc c041f0fd7f Fixed copilot migration file 2021-10-20 18:42:09 +01:00
benarc f68f45b6b1 Fixed in a hacky way, but works 2021-10-20 18:28:54 +01:00
benarc 8f44747d6c Mgration for changing show_price to TEXT, doesnt work :( 2021-10-20 18:23:03 +01:00
Tiago vasconcelos b97cef0e37 init clean up 2021-10-20 16:34:00 +01:00
benarc 4403186441 Cant call satspayserver function 2021-10-20 12:43:31 +01:00
Tiago vasconcelos 65b926ad08 needs testing 2021-10-20 12:13:03 +01:00
Tiago vasconcelos 46400448f1 Merge remote-tracking branch 'origin/FastAPI' into FastAPI 2021-10-20 12:08:23 +01:00
Tiago vasconcelos a840970366 fix require admin key endpoints 2021-10-20 12:03:11 +01:00
benarc bf1a711fb9 deleted more unused endpoints 2021-10-20 11:59:36 +01:00
benarc 0d3e18bf71 Removed unused endpoint 2021-10-20 11:57:09 +01:00
benarc 14ebf72255 Stuck getting user to create satspay charge from tipjar 2021-10-20 11:51:48 +01:00
benarc 40f3e8b210 Tipjar initial 2021-10-20 11:14:56 +01:00
Tiago vasconcelos 39eb9d0d70 events fix tickets not showing in index.html 2021-10-20 10:54:17 +01:00
ArcandGitHub 709bda6c8a Merge pull request #360 from arcbtc/FastAPI
latest commits. Most extensions added
2021-10-20 08:22:17 +01:00
Ben Arc 552fa8edc6 LNURLPoS loading, form working 2021-10-20 06:49:59 +01:00
Ben Arc 37b14e3d6d Satsdice working 2021-10-20 04:52:33 +01:00
Ben Arc 19df2f888b Added decode lnurl and extra payment check 2021-10-20 03:59:24 +01:00
Ben Arc b6f5052da0 trying to fix payment check 2021-10-20 03:28:31 +01:00
Ben Arc e0db0bc6cd Satsdice working, although invoices are not being seen as paid 2021-10-20 03:08:43 +01:00
Ben Arc 47b3e537f4 satsdice lnurl pain 2021-10-19 23:05:29 +01:00
Ben Arc aaaa4ce8e7 Added ngrok for easy LNURL developing 2021-10-19 21:13:26 +01:00
Ben Arc 4a9b5840ab Ngrok init 2021-10-19 21:08:12 +01:00
Ben Arc fc5885d52b creating but not fetching 2021-10-19 20:54:02 +01:00
Tiago vasconcelos facd94f59e fix create and update 2021-10-19 17:02:22 +01:00
Tiago vasconcelos 41abbb44f7 added new error message (detail) 2021-10-19 17:01:57 +01:00
Tiago vasconcelos 3040016323 events untested commit 2021-10-19 16:12:03 +01:00
Tiago vasconcelos ca1ad33c7b splitpayments working - hard one 2021-10-18 19:39:35 +01:00
Tiago vasconcelos cf23e56dc5 cleanup/fix breaking imports 2021-10-18 16:23:51 +01:00
Tiago vasconcelos 4739a0811d added admin key required 2021-10-18 16:06:06 +01:00
benarc cfd37ec31e splitpayments auth issues using WalletAdminKeyChecker 2021-10-18 13:35:31 +01:00
benarc cf6fae2ca7 Added internal payment listener to paid invoices and splitpayments 2021-10-18 13:24:32 +01:00
benarc 3285e9d3c9 Splitpayments booting, but not sure how to handle internal pays 2021-10-18 13:02:28 +01:00
benarc a574743080 splitpayment init 2021-10-18 12:34:45 +01:00
Tiago vasconcelos 167a32fed9 core already has default memo 2021-10-18 12:25:43 +01:00
Tiago vasconcelos 19a50e3638 add default memo 2021-10-18 12:23:57 +01:00
Tiago vasconcelos 1fc4b3b3e4 clean up 2021-10-18 12:08:41 +01:00
Tiago vasconcelos 1aeb4fe70c removed offensive print and clean up 2021-10-18 12:07:08 +01:00
Tiago vasconcelos 8dfd334daa clean up file 2021-10-18 11:58:21 +01:00
Tiago vasconcelos 189eed850a fix copilot memo None 2021-10-18 11:51:51 +01:00
Tiago vasconcelos 59de66f00b Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-10-18 10:58:20 +01:00
Tiago vasconcelos 70885ce160 clean up (black, sort) 2021-10-18 10:58:09 +01:00
benarc 0d781f899c copilot not triggering animations, listeners 2021-10-18 10:41:41 +01:00
benarc 0cf11caaad copilot payments not triggering animations
listener issue
2021-10-18 10:31:30 +01:00
benarc 15cd84652e copilot payments not triggering animations 2021-10-18 10:28:31 +01:00
benarc 762f0ba404 copilot lnurls fetching 2021-10-17 23:19:42 +01:00
benarc 75ea724d12 Websocket fully working 2021-10-17 21:15:03 +01:00
benarc 1d3bb016a2 I want them to turn black 2021-10-17 18:33:29 +01:00
Stefan Stammberger 70facdaa93 fix(lndhub): require admin key for sending funds 2021-10-17 10:30:59 +02:00
Tiago vasconcelos 2282e79f4d hack to stop paying invoices if not admin key 2021-10-16 22:27:21 +01:00
Stefan Stammberger cb6f9e1f72 fix: copilot websocket connections 2021-10-16 13:35:50 +02:00
Stefan Stammberger 570e8d28dd fix: lndhub auth handling 2021-10-15 19:55:24 +02:00
Tiago vasconcelos 6dea1f7271 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-10-15 17:06:04 +01:00
Tiago vasconcelos 8e396beb44 fusion's fix (not working) 2021-10-15 17:05:38 +01:00
Tiago vasconcelos 36d55db4fc fix argument order 2021-10-15 16:39:45 +01:00
Tiago vasconcelos 43653cb84d auth bearer fix
x-api-key now says unauthorized
2021-10-15 16:21:05 +01:00
Tiago vasconcelos 9096ed38b9 fix routing issues 2021-10-15 16:20:23 +01:00
benarc 982415f013 Satsdice error, confused over url_for 2021-10-15 14:28:41 +01:00
benarc 85feb0de86 Merge branch 'fastapisatsdice' into FastAPI 2021-10-14 23:08:25 +01:00
benarc b9ff95a9cf Satsdice loading, needs finishing 2021-10-14 23:08:09 +01:00
benarc a8451e6f50 Merge branch 'FastAPI' into fastapisatsdice 2021-10-14 22:38:14 +01:00
Tiago vasconcelos 6935589dad watchonly done 2021-10-14 22:30:47 +01:00
Tiago vasconcelos ec4117a5f4 satspay done 2021-10-14 22:30:29 +01:00
Tiago vasconcelos e939666107 satspay initial converstion 2021-10-14 11:45:56 +01:00
Tiago vasconcelos ec89244d7f whatchonly ext added 2021-10-14 11:45:30 +01:00
Tiago vasconcelos 43f36afeb0 satspay ext added 2021-10-14 10:02:02 +01:00
Tiago vasconcelos 4686f87626 withdraw route order fixed 2021-10-14 10:00:33 +01:00
benarc 33cfbe87bb views converted 2021-10-13 21:51:21 +01:00
benarc 8db9459501 Init 2021-10-13 17:08:48 +01:00
ArcandGitHub a914840351 Merge pull request #4 from arcbtc/fastapilndhub
LNDHub loading, prob not working
2021-10-13 17:49:06 +02:00
benarc 08bc771fbe LNDHub loading, prob not working 2021-10-13 16:47:24 +01:00
benarc 0c6cd94154 Websockets not working 2021-10-13 13:23:58 +01:00
benarc 82a9cba871 Copilot working without websockets 2021-10-13 12:37:10 +01:00
benarc 16f9f1612f Pydantic being quietly pydantic 2021-10-13 11:20:40 +01:00
benarc d5a4dc801f copilot still not working 2021-10-13 10:43:11 +01:00
Ben Arc 771c7c25c1 Not making db entry 2021-10-13 08:35:25 +01:00
Ben Arc a0be1a5017 Copilot loads 2021-10-12 23:26:01 +01:00
ArcandGitHub 1bd08d3a44 Merge pull request #3 from arcbtc/fastapicopilot
Adding copilot
2021-10-13 00:14:52 +02:00
Ben Arc e1b4df869f copilot starts, templates need adjusting 2021-10-12 23:11:10 +01:00
Ben Arc 1346ad12f1 Endpoints changed but still not working 2021-10-12 22:34:43 +01:00
Ben Arc aeee469c64 converted models 2021-10-12 20:24:00 +01:00
Ben Arc c749b7d11e init 2021-10-12 19:54:35 +01:00
Ben Arc 567b8e3c48 Jukebox done 2021-10-12 19:47:01 +01:00
Ben Arc fb09b94e25 Pretty confirm for TPoS 2021-10-12 19:02:00 +01:00
Ben Arc 25616031eb Merge remote-tracking branch 'arcbtc/FastAPI' into FastAPI 2021-10-12 18:54:54 +01:00
Ben Arc cace0892f2 jukebox working(ish) 2021-10-12 18:54:40 +01:00
Tiago vasconcelos 04462f337f usermanager working 2021-10-12 17:04:49 +01:00
Tiago vasconcelos f4dfc1b689 usermanager views_api 2021-10-12 10:57:15 +01:00
Tiago vasconcelos ce4235f858 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-10-12 10:38:29 +01:00
Tiago vasconcelos e5b22ead0c initial work 2021-10-12 10:38:09 +01:00
benarc a33b24d6dc Liberated HTTPStatus.OKs 2021-10-12 09:59:01 +01:00
ArcandGitHub e6b032a4b6 Merge pull request #2 from arcbtc/fastapijukebox
Jukebox. No idea why its not working
2021-10-12 10:48:27 +02:00
benarc f94543943f latest 2021-10-12 09:44:41 +01:00
benarc ca3b1d7455 fires up ok 2021-10-12 09:01:54 +01:00
Tiago vasconcelos 706d584332 added usermanager extension 2021-10-11 11:52:21 +01:00
Tiago vasconcelos de97c0f696 lnurlp import execption
withdraw not working
2021-10-11 11:46:36 +01:00
benarc 9ff1313b0b should be working 2021-10-10 23:57:15 +01:00
Stefan Stammberger 9a18720a96 fix: embeddable img link in withdraw extension 2021-10-10 18:49:41 +02:00
benarc 7dd29b3eca initial 2021-10-10 16:23:39 +01:00
ArcandGitHub 1adabcd7e7 Merge pull request #357 from arcbtc/FastAPI
Latest updates
2021-10-10 12:11:18 +02:00
Stefan Stammberger c1a82dad9d fix: lnurlp templates and path functions
While rendering the tempaltes the link.lnurl() function was not found
as no request object was passed.

Compute the lnurl before rendering the template. This way we can avoid
computing it multiple times and do not to have to pass the request object
in the template.
2021-10-10 11:05:43 +02:00
Tiago vasconcelos d319aca6cf withdraw bug fixing/can't withdraw 2021-10-08 17:10:25 +01:00
Tiago vasconcelos 7e3b0c08dc lnurlp cleaned and finished 2021-10-08 12:10:43 +01:00
Tiago vasconcelos 7b93299ef7 axios query params fix 2021-10-08 11:03:55 +01:00
Tiago vasconcelos 0a85bae405 undo stringify 2021-10-07 11:01:26 +01:00
Tiago vasconcelos 683f138f1b invoice gets undefined 2021-10-06 19:43:57 +01:00
Tiago vasconcelos d9aaae7872 added tpos extension 2021-10-06 14:49:00 +01:00
Tiago vasconcelos 46ab9960ad url_for issues 2021-10-06 14:40:12 +01:00
Tiago vasconcelos 7646bbefd5 can't seem to make url_for work 2021-10-06 11:10:24 +01:00
Tiago vasconcelos 81826f3c13 fix: lnurlp, withdraw 2021-10-05 09:19:21 +01:00
ArcandGitHub ac52800e3a Merge pull request #351 from arcbtc/FastAPI
Latest from arcbtc branch
2021-10-02 19:12:42 +02:00
Tiago vasconcelos 6edac8ae8d remove extensions 2021-10-01 10:49:11 +01:00
Tiago vasconcelos 4120a51e0c Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-10-01 10:28:21 +01:00
Stefan Stammberger dba988a969 fix: reactivate catch-all exception handler 2021-09-30 19:16:38 +02:00
Stefan Stammberger 27e1781464 chore: update requirements.txt 2021-09-30 18:52:37 +02:00
Tiago vasconcelos 3df8e22ba2 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-09-30 14:42:19 +01:00
Tiago vasconcelos c0280838e7 ext: withdraw ported + lnurlp fixes 2021-09-30 14:42:04 +01:00
Stefan Stammberger 87c784aa0c fix: create and pay a ticket code 2021-09-29 20:57:35 +02:00
Stefan Stammberger 534dfdb4e0 fix: startup of lntickets listeners 2021-09-29 20:44:00 +02:00
Tiago vasconcelos 65aa1b24d8 ext: widthraw - views_api 2021-09-29 11:04:37 +01:00
Tiago vasconcelos 4c724e6f01 typos fix 2021-09-29 11:03:02 +01:00
Tiago vasconcelos 0e1a21f431 merged fusion fixes 2021-09-29 10:44:41 +01:00
Tiago vasconcelos 20c44cd0a0 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-09-29 10:43:19 +01:00
Stefan Stammberger f827d2ce18 fix: fetch tickets and forms JS and endpoints 2021-09-28 22:03:45 +02:00
Stefan Stammberger 85d9a3fcc3 fix: some errors in lnticket ext 2021-09-28 21:18:15 +02:00
Stefan Stammberger 2a4314ca84 fix: raise bad req error on call without X-API-key 2021-09-28 21:13:04 +02:00
Stefan Stammberger 8c5989d465 fix: crash when an ext doesn't have static files 2021-09-28 21:10:51 +02:00
Tiago vasconcelos dcad7ea5fa lnurlp fastAPI port 2021-09-28 18:10:22 +01:00
Tiago vasconcelos de4f7fd07a tickets fastAPI fixes 2021-09-28 18:10:06 +01:00
Tiago vasconcelos ca5ee4ed4f tickets fastAPI 2021-09-27 21:02:50 +01:00
Tiago vasconcelos 3a3ef4cd22 tickets fastAPI: init 2021-09-27 20:33:18 +01:00
Tiago vasconcelos 0891611d2b fastAPI port views_api 2021-09-27 15:52:08 +01:00
Stefan Stammberger a5c0a2bb50 fix: offlineshop ext lnurl handling 2021-09-19 13:34:31 +02:00
Stefan Stammberger c5d96d21e9 fix: core pay via lnurl endpoint 2021-09-19 13:25:39 +02:00
Stefan Stammberger c906421088 fix: add Jinja2 as an dependency 2021-09-19 13:24:19 +02:00
Stefan Stammberger 32ca6289df fix: broken payment API implementation 2021-09-19 09:31:16 +02:00
ArcandGitHub 7af04e493f Merge pull request #343 from arcbtc/FastAPI
Offline shop working
2021-09-17 11:29:43 +01:00
Stefan Stammberger e3c7ca0726 feat: update offlineshop extension 2021-09-16 19:42:05 +02:00
Stefan Stammberger 24bb2e0dc9 docs: add a section how to define path parameters 2021-09-16 19:25:01 +02:00
ArcandGitHub ad3e8daddd Merge pull request #339 from arcbtc/FastAPI
Wallet and offlineshop working
2021-09-13 12:53:02 +01:00
Stefan Stammberger 3bae5c92c2 fix: /extensions endpoint 2021-09-11 20:44:22 +02:00
Stefan Stammberger 7b69852acc fix: make check_user_exists() work with FastAPI 2021-09-11 15:18:09 +02:00
Stefan Stammberger c2551bd765 docs: add another old way of raising exceptions 2021-09-11 12:28:29 +02:00
Stefan Stammberger 9e76668269 fix: send payments via Wallet UI 2021-09-11 11:47:05 +02:00
Stefan Stammberger 63d0242685 fix: more return types 2021-09-11 11:02:48 +02:00
Stefan Stammberger d8d8c6b454 docs: add a FastAPI transition documentation 2021-09-10 21:41:37 +02:00
Stefan Stammberger fa08177317 fix: FastAPify how data or exceptions are returned
FastAPI handles returning HTTPStatus codes differently than Quart did
2021-09-10 21:40:14 +02:00
ArcandGitHub 9e68a242e8 Merge pull request #328 from arcbtc/FastAPI
latest fusion44 commits
2021-09-02 10:47:57 +01:00
Stefan Stammberger d9849d43d2 refactor: replace Trio with asyncio/uvloop 2021-08-30 19:55:02 +02:00
Stefan Stammberger fe79709698 fix: several more API calls restored 2021-08-29 19:38:42 +02:00
Stefan Stammberger 5ae124408e fix: wallet page
Javascript was incompatible because of the switch to FastAPI.
2021-08-28 13:55:31 +02:00
Stefan Stammberger ee775b66ef fix: settings import 2021-08-28 11:16:59 +02:00
Stefan Stammberger 3e5af8c1d1 refactor: purge Quart from the codebase
Most functionality is still broken
2021-08-27 20:54:42 +02:00
Tiago vasconcelos ede038976f refactor http status responses 2021-08-25 15:44:18 +01:00
Stefan Stammberger f119053953 fix: main page and creating a user and a wallet
The wallet page will still not renders correctly, but the backend does
create the user his first wallet.
2021-08-24 21:23:18 +02:00
Stefan Stammberger de4d3b012c fix: invoke error page correctly 2021-08-24 19:14:04 +02:00
Stefan Stammberger 57fb6c4b11 Merge branch 'FastAPI' of github.com:arcbtc/lnbits into FastAPI 2021-08-23 21:17:50 +02:00
Stefan Stammberger 6f38fdc39b feat: move all non-api routes to their own router
This unclutters the docs page a bit by making html routes collapsible.
2021-08-23 21:17:46 +02:00
Ben Arc 449cbfed89 Error page 2021-08-23 19:19:43 +01:00
ArcandGitHub 89ffc621c4 Merge pull request #316 from arcbtc/FastAPI
Front page loading!
2021-08-23 18:23:58 +01:00
Stefan Stammberger 0806ccfbbd fix: missing static file serving folder 2021-08-23 18:51:17 +02:00
Tiago vasconcelos 83a9dc2a3e syntax refactoring up to diagonalley 2021-08-23 10:18:52 +01:00
ArcandGitHub e350f639a5 Merge pull request #314 from arcbtc/FastAPI
Most extensions done
2021-08-22 23:08:39 +01:00
Ben Arc e03eb3c82b Fixed formatting 2021-08-22 23:05:39 +01:00
ArcandGitHub 0b132ce928 Merge pull request #313 from arcbtc/FastAPI
Most extensions done
2021-08-22 22:17:55 +01:00
Stefan Stammberger bbdd41ef87 fix: remove tag as "Apps" to avoid clutter in docs 2021-08-22 20:22:18 +02:00
Stefan Stammberger 938fc54af3 feat: switch from Quart to FastAPI part I 2021-08-22 20:07:24 +02:00
Tiago vasconcelos fc68e0a6da fastAPI refactoring 2021-08-22 12:17:43 +01:00
Tiago vasconcelos 3e0fd39175 some syntax refactoring 2021-08-22 12:16:31 +01:00
ArcandGitHub 064994d67e Merge pull request #311 from arcbtc/FastAPI
Most extensions done
2021-08-22 11:41:18 +01:00
Tiago vasconcelos a9ab41701f usemanager syntax error 2021-08-22 11:35:08 +01:00
Tiago vasconcelos 7f917b8ad5 lntickets fastAPI refactoring 2021-08-22 11:31:04 +01:00
Tiago vasconcelos 5495dac6d3 satspay started;views_api refactored 2021-08-22 10:29:01 +01:00
Ben Arc 2fb16d0382 Trying to add query params 2021-08-21 19:04:10 +01:00
Stefan Stammberger d20c3ce119 fix: syntax errors 2021-08-21 12:14:23 +02:00
ArcandGitHub 266e220f94 Merge pull request #310 from arcbtc/FastAPI
Converted views
2021-08-21 02:08:30 +01:00
Ben Arc a6f0a36664 Converted all views 2021-08-21 02:05:45 +01:00
ArcandGitHub c8e722c234 Merge pull request #309 from arcbtc/FastAPI
Converted views
2021-08-21 02:01:06 +01:00
Ben Arc bbdb96f4ac Converted views 2021-08-21 01:55:07 +01:00
ArcandGitHub 5671348fea Merge pull request #308 from arcbtc/FastAPI
Started on extensions and projects
2021-08-21 00:42:18 +01:00
Ben Arc d3d24abb17 removed json_encoders 2021-08-21 00:39:31 +01:00
ArcandGitHub ddab1df8c4 Merge pull request #307 from arcbtc/FastAPI
Started on extensions and projects
2021-08-21 00:39:18 +01:00
Ben Arc a9dc087f61 All extensions semi-switched 2021-08-21 00:34:48 +01:00
Ben Arc 8deea85999 fixing withdraw 2021-08-20 22:21:15 +01:00
ArcandGitHub 0baa34ec2f Merge pull request #306 from arcbtc/FastAPI
Started on extensions and projects
2021-08-20 21:36:40 +01:00
Ben Arc c4b37c6508 Converted some core stuff 2021-08-20 21:31:01 +01:00
Ben Arc bdbdc1601b Merge remote-tracking branch 'arcbtc/FastAPI' into benarcfastapi 2021-08-20 20:12:07 +01:00
Ben Arc c824155ea4 Working on jukebox 2021-08-20 20:12:03 +01:00
Stefan Stammberger da20280fd3 Merge branch 'FastAPI' of github.com:arcbtc/lnbits into FastAPI 2021-08-20 21:02:40 +02:00
Stefan Stammberger 37a7950f0f fix: syntax errors 2021-08-20 20:54:59 +02:00
Ben Arc 76cb93b276 Started watchonly 2021-08-20 19:48:44 +01:00
Ben Arc 58a498d9bf Merge remote-tracking branch 'arcbtc/FastAPI' into benarcfastapi 2021-08-20 18:43:20 +01:00
Ben Arc 09eebd8351 Starting jukebox 2021-08-20 18:43:11 +01:00
Tiago vasconcelos 8a30c15f61 Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI 2021-08-20 17:51:37 +01:00
Tiago vasconcelos 12846f5cac testing push 2021-08-20 17:51:30 +01:00
Ben Arc 5eb28f2637 Merge remote-tracking branch 'arcbtc/FastAPI' into benarcfastapi 2021-08-20 17:47:14 +01:00
Ben Arc 539ae10c6f started events 2021-08-20 17:46:57 +01:00
Tiago VasconcelosandGitHub 50edc5cd5a Merge pull request #1 from talvasconcelos/FastAPI
Fast api
2021-08-20 17:34:36 +01:00
Tiago vasconcelos 686fd0ab29 Merge remote-tracking branch 'upstream/FastAPI' into FastAPI 2021-08-20 17:15:41 +01:00
Ben Arc 9ea3c51b92 Changed routs to request types 2021-08-20 17:15:04 +01:00
Tiago vasconcelos edf748d690 refactor for fastAPI 2021-08-20 17:14:44 +01:00
Tiago vasconcelos de0f6389ae change class to pydantic 2021-08-20 17:14:26 +01:00
Ben Arc 22b57d99d5 Added CreateData model to withdraw 2021-08-20 16:47:55 +01:00
Ben Arc ab78a9cd8a Merge branch 'FastAPI' into benarcfastapi 2021-08-20 16:36:31 +01:00
Ben Arc e36cd18640 Started converting withdraw 2021-08-20 16:35:24 +01:00
Ben Arc 861e2fbf67 sfabsbd 2021-08-20 16:15:49 +01:00
Ben Arc 9c3442ea43 Attempt at replacing schema 2021-08-20 15:27:57 +01:00
Ben Arc 8dea1e28f0 Added pydantic to models 2021-08-20 14:46:08 +01:00
Ben Arc c525655c15 Removed tpos for Tal 2021-08-20 12:47:46 +01:00
Ben Arc fe123d6d31 Put extensions back so we can start converting 2021-08-20 12:44:03 +01:00
Tiago vasconcelos 0cd828d43f added tpos back 2021-08-20 10:34:52 +01:00
Ben Arc bfd9ca68e4 Broken, trying to convert 2021-08-18 21:40:50 +01:00
Ben Arc 0eccfb83f0 Trying list data validation 2021-08-16 20:30:11 +01:00
Ben Arc 1118b0ba56 Started converting endpoints 2021-08-16 19:27:39 +01:00
Ben Arc 9c313586bd removed all but one extension, so we can focus on core 2021-08-16 18:12:48 +01:00
Ben Arc afc2b78a25 Fixed lnticket migrate bug 2021-08-16 10:03:10 +01:00
Ben Arc c2a11f5877 tickets migrate bug fix 2021-08-16 09:44:35 +01:00
Ben Arc ee9490ab68 Fixed multiple play bug in jukebox 2021-08-16 09:39:14 +01:00
Ben Arc 52cb381bd5 Changed form table name 2021-08-16 09:13:16 +01:00
Ben Arc 094943e293 Switched jukebox back to polling until it can use listeners properly 2021-08-16 09:00:20 +01:00
Ben Arc 734bf1bcd5 made polling longer 2021-08-16 07:36:02 +01:00
Ben Arc 3dd94dd710 Changed webhook to actually optional 2021-08-16 07:33:47 +01:00
ArcandGitHub 1359c1ee39 Merge pull request #295 from lnbits/lnticketsflat
Returns lnticket to polling, until listeners are properly tested, and adds a flat rate
2021-08-16 07:24:18 +01:00
Ben Arc 36af425c1b Changed back to polling
Once a stable listener alternative is production ready it can be added back
2021-08-16 07:15:59 +01:00
cd2357andfiatjaf b53ad5b64a Create Invoice popup: Mark memo field as required
Both amount and memo are required by the API, but only the amount field is marked as such in the UI. This commit also marks the memo field as required.
2021-08-14 06:01:21 -03:00
Ben Arc 7bdbe08732 Merge branch 'master' into lnticketsflat 2021-08-13 23:24:12 +01:00
Ben Arc 3fda8bf06f flatrate added 2021-08-13 23:23:18 +01:00
Tiago vasconcelosandfiatjaf 20ecfb4810 Fix for postgres compatibility 2021-08-13 18:25:18 -03:00
Tiago vasconcelosandfiatjaf da7f7fb618 fix js error on fetch copilots 2021-08-13 18:25:18 -03:00
Tiago vasconcelosandfiatjaf 4ce0ed63c0 fix livestream for postgres compatibility
fix producers create returning id
2021-08-13 13:32:49 -03:00
fiatjaf 1c0a4e252a explicit postgres type numbers in anti-conversion table. 2021-08-11 22:53:34 -03:00
fiatjaf 8f2b42309d revert b7ed7fab59 2021-08-11 22:53:11 -03:00
fiatjaf 26b3599325 fix insertion bugs/incompatibilities with lastrowid stuff. 2021-08-11 22:19:27 -03:00
Tiago VasconcelosandGitHub b7ed7fab59 (TEMP) fix tickets not showing after payment (#292) 2021-08-11 13:52:54 -03:00
fiatjaf 6312b5b564 fix compatibility on REFERENCES statement on streamalerts table creation. 2021-08-11 10:23:02 -03:00
Ben Arc 18761ea1da temp fix for running migrate 2021-08-11 12:33:56 +01:00
fiatjaf b96a6661f8 fix "requesting" display for addresses. 2021-08-10 08:11:51 -03:00
fiatjaf 9c31d06112 remove quotes from streamalerts table names. 2021-08-10 07:40:04 -03:00
fiatjaf a019d29b9b support pay-to-identifier. 2021-08-10 07:25:51 -03:00
fiatjaf 2bcc01d640 streamalerts: fix db queries so postgres can work. 2021-08-09 20:29:37 -03:00
benarc 4d8452c7c9 Fixed colours + prettier 2021-08-09 20:15:54 +01:00
0697a1660f Stream Alerts: Follow new dynamic naming convention (#289)
Co-authored-by: Fitti <fitti@fitti.io>
2021-08-07 11:14:27 -03:00
oriolpontandfiatjaf 19d432b404 forward the port as set in PORT env var 2021-08-06 12:38:58 -03:00
Tiago vasconcelosandfiatjaf 723c9ea3ab cleanup 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf b3856d5aff allow wallet rename issue #141 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf 16f8071731 fix issue #267 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf a32e43b2bb fix having LND_GRPC_MACAROON as HEXSTRING issue #268 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf 9b89329029 add usermanager to the curl api examples 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf b4ea4c75ad fix issue #286 2021-08-06 12:37:30 -03:00
Tiago vasconcelosandfiatjaf d582ee87e5 fix postgres not showing payments 2021-08-05 08:57:50 -03:00
0813adb70d Stream Alerts: Enforce sensible length limits on Name and Message (#285)
Co-authored-by: Fitti <fitti@fitti.io>
2021-08-04 11:32:51 -03:00
FittiandGitHub baf3b76c33 Stream Alerts: Fix donations table and default donor name "Anonymous" (#283) 2021-08-03 15:54:55 -03:00
fiatjafandGitHub 4a84204523 Merge pull request #281 from talvasconcelos/fix/streamAlertsUI
Fix UI issue on service creation
2021-08-02 07:05:53 -03:00
Tiago vasconcelos e3764e384a clean up comments and logs 2021-08-02 10:58:04 +01:00
Tiago vasconcelos 91f082eb69 fixed create_service from sending html and showing correctly on create 2021-08-02 10:25:06 +01:00
fiatjafandGitHub 32764d1bad Merge pull request #227 from Fittiboy/TwitchAlerts
Stream Alerts extension
2021-08-02 05:23:25 -03:00
fiatjaf e0496fb244 fix postgres type translation bug and add some logs. 2021-08-01 10:30:27 -03:00
Fitti 89f54143f7 Fix typo 2021-08-01 09:35:15 +02:00
FittiandGitHub 989f4eef8e Merge branch 'lnbits:master' into TwitchAlerts 2021-08-01 09:07:39 +02:00
Fitti bac5aeaedb Update README.md 2021-08-01 07:00:45 +02:00
Fitti 663cd42f5f Update README.md 2021-08-01 06:59:10 +02:00
Fitti 93a2d9c9a3 Uri -> URI 2021-08-01 06:13:29 +02:00
Fitti 716c3f934b Add Streamlabs Redirect Uri 2021-08-01 06:12:35 +02:00
Fitti a18806ecf3 Fix onchain support 2021-08-01 05:45:36 +02:00
Fitti 5ddcb60525 Enable onchain wallet support 2021-08-01 05:22:28 +02:00
Fitti 7444be8c54 Rename TwitchAlerts to StreamAlerts 2021-08-01 04:54:43 +02:00
fiatjaf 83137ba0a0 print handled exceptions. 2021-07-31 08:15:28 -03:00
fiatjaf b1b487bc48 catch everything and restart in main background tasks. 2021-07-30 19:29:54 -03:00
Ben Arc aa1c83cdc4 added dynamic titles for continuity 2021-07-25 14:26:20 +01:00
fiatjaf 2b82985d36 fix issue #273 2021-07-20 16:31:09 -03:00
davidfrayneandfiatjaf 3331dfeab6 Fix payments filter on wallets.html 2021-07-12 10:52:31 -03:00
ArcandGitHub 6350a74ab9 Added telegram link 2021-07-10 17:59:14 +01:00
ArcandGitHub 6ba8aa65e0 Added telegram link 2021-07-10 17:57:05 +01:00
Ben Arc bba3cbc3e4 Ngrok benched
Too many people complaining about it downloading ngrok, so its now been disabled via its config
2021-07-07 23:33:55 +01:00
ArcandGitHub ca29c3f238 Update .env.example 2021-07-07 11:06:11 +01:00
Ben Arc b02952d99d Added umbrel/tor guide 2021-07-07 10:58:21 +01:00
ArcandGitHub ad64972ddf Update installation.md 2021-07-07 10:57:45 +01:00
Ben Arc a9163a1456 Added umbrel/tor guide 2021-07-07 10:53:53 +01:00
ArcandGitHub 2b68ab301e Merge pull request #247 from talvasconcelos/fix/allowDisableAllExt
Small fix to disable all extensions
2021-07-07 10:26:44 +01:00
Ben Arc 85bf0ebb15 black/prettier 2021-07-07 10:19:16 +01:00
Ben Arc dd5080ac5a Merge branch 'master' into TwitchAlerts 2021-07-07 09:57:25 +01:00
Fitti 7c1ff1ff37 Format with yapf 2021-07-07 01:28:39 +02:00
Tiago vasconcelos 3f6393a943 Small fix to disable all extensions 2021-07-05 10:02:36 +01:00
fiatjaf b7332bea64 fix example extension migration template. 2021-07-03 17:57:09 -03:00
fiatjaf 81e3424232 add hivemind placeholder extension. 2021-07-03 17:56:55 -03:00
fiatjaf 5ac81a7849 update material fonts. 2021-07-03 17:44:46 -03:00
fiatjaf f6e255d9be remove one extra core.schema. 2021-07-03 15:39:58 -03:00
Fitti 435787ad93 Cleanup, Comments, Docstrings 2021-07-03 18:01:04 +02:00
ArcandGitHub 61c300c393 Merge pull request #245 from supertestnet/master
Add ngrok extension
2021-07-03 16:54:01 +01:00
Ben Arc 0ea661babb disabled ngrok 2021-07-03 16:51:11 +01:00
Ben Arc e7ea390d10 disabled in .env 2021-07-03 16:49:39 +01:00
Ben Arc 0e209845ed Sidebar back 2021-07-03 16:48:23 +01:00
Ben Arc 5003a89a48 small fix 2021-07-03 16:16:28 +01:00
Ben Arc 7a18a44948 needed migration file 2021-07-03 16:12:07 +01:00
Ben Arc 4fc947adff Merge branch 'master' into super 2021-07-03 16:08:50 +01:00
Ben Arc 0c44c0c048 Black 2021-07-03 16:08:18 +01:00
Ben Arc 9b483fe5ff Merge branch 'supermaster' into super 2021-07-03 16:05:05 +01:00
Ben Arc 03f0df681b ran black 2021-07-03 16:04:45 +01:00
Ben Arc 819fc43328 removed db reference 2021-07-03 15:50:40 +01:00
Ben Arc ad618c1a7b woops fixed bug 2021-07-03 15:47:53 +01:00
Ben Arc 16b10d8b7c Maybe 2021-07-03 15:33:05 +01:00
Ben Arc 7eba75c0a0 Deleted all the bits we dont need 2021-07-03 15:29:40 +01:00
Ben Arc 64f72f90eb Added creditation 2021-07-03 15:05:17 +01:00
Ben Arc 483c4b6586 ran black 2021-07-03 15:01:16 +01:00
Ben Arc 8619da29cf fixed bug 2021-07-03 15:00:08 +01:00
supertestnetandGitHub 117ae1aec3 Update requirements.txt 2021-07-03 09:18:22 -04:00
supertestnetandGitHub a9c5bb93c3 Update __init__.py 2021-07-03 09:10:46 -04:00
supertestnetandGitHub 24b20a5b6c Update config.json 2021-07-03 09:10:21 -04:00
supertestnetandGitHub 171247853c Update views_api.py 2021-07-03 09:09:54 -04:00
supertestnetandGitHub ba364f1505 Update views.py 2021-07-03 09:09:35 -04:00
supertestnetandGitHub c5ed8e0391 Create index.html 2021-07-03 09:09:08 -04:00
supertestnetandGitHub 541f655f4f Create .gitkeep 2021-07-03 09:08:21 -04:00
supertestnetandGitHub b2a337fb0b Delete lnbits/extensions/ngrok/pycache directory 2021-07-03 09:08:04 -04:00
supertestnetandGitHub e559ede16c Create .gitkeep 2021-07-03 09:07:37 -04:00
supertestnetandGitHub 10ecaf5d24 Update README.md 2021-07-03 09:07:02 -04:00
supertestnetandGitHub b53db465c0 Update README.md 2021-07-03 09:06:40 -04:00
supertestnetandGitHub bb9299b3b2 Add files via upload 2021-07-03 09:05:48 -04:00
supertestnetandGitHub 674dc0f7fc Create .gitkeep 2021-07-03 08:59:17 -04:00
fiatjaf 50e3d3803b add more db instructions to .env.example. 2021-07-02 19:10:57 -03:00
fiatjaf ffadce02b0 support cockroachdb. 2021-07-02 18:32:58 -03:00
fiatjaf 2466cd59db adjust copilot to postgres branch. 2021-07-02 17:53:53 -03:00
fiatjaf 46eab3b35e psycopg2 only imported when postgres. 2021-07-02 17:34:33 -03:00
fiatjaf e2d43e786f fix wrong schema for apipayments usage inside extensions. 2021-07-02 17:34:33 -03:00
fiatjaf 2f309c9863 postgres support. 2021-07-02 17:34:31 -03:00
Ben Arc eeb3e66529 Made title clickable 2021-07-02 12:52:27 +01:00
Ben Arc ae0849143b Easier to customise title/tagline
Also added description that can replace frontpage blurb
2021-07-02 12:31:05 +01:00
ArcandGitHub c1c1332e37 Merge pull request #242 from talvasconcelos/feature/readthemeenv
Fix theme choosing
2021-07-01 23:51:09 +01:00
Tiago vasconcelos 277460edb7 final push for themes 2021-07-01 12:34:50 +01:00
Tiago vasconcelos 2cd6877645 Merge branch 'master' into feature/readthemeenv 2021-07-01 12:05:40 +01:00
Tiago vasconcelos 4493e93890 small fix for theme picking/seting 2021-07-01 11:54:45 +01:00
ArcandGitHub 3562ceafe3 Merge pull request #223 from talvasconcelos/jukebox/addlistener
Jukebox/addlistener
2021-07-01 00:42:53 +01:00
ArcandGitHub 7c56501225 Merge branch 'master' into jukebox/addlistener 2021-07-01 00:42:34 +01:00
ArcandGitHub 5eb208b8de Merge pull request #217 from talvasconcelos/fix/jukeboxfixes
Update for jukebox extension readme
2021-07-01 00:34:25 +01:00
ArcandGitHub aee3640171 Merge pull request #239 from cryptoteun/master
Fix for #238 Minor issues in API documentation of lnurlp extension
2021-07-01 00:32:01 +01:00
Ben Arc e8c2a5f968 Added subdomain readme link 2021-07-01 00:20:41 +01:00
ArcandGitHub a6835840a2 Merge pull request #241 from talvasconcelos/feature/readthemeenv
Read .env themeing colors
2021-07-01 00:08:34 +01:00
Ben Arc c778149d4f Tweaked few more color themes 2021-06-30 23:14:39 +01:00
Ben Arc bf26bf963f added theme color 2021-06-30 22:48:47 +01:00
Ben Arc 27a0b64b03 made flamingo darker 2021-06-30 17:34:22 +01:00
Tiago vasconcelos 87d12634f7 Read .env themeing colors
- allowed/availabe themes defined by admin
2021-06-30 17:00:56 +01:00
Ben Arc ff4956bdfb fixed vue error 2021-06-30 16:44:18 +01:00
ArcandGitHub ca49253d90 Merge pull request #237 from lnbits/colours
Color themes
2021-06-30 13:52:38 +01:00
Ben Arc e8e4116cdf Improved pink 2021-06-30 13:42:47 +01:00
Ben Arc 55c564fe21 Fixed jukebox to match style 2021-06-30 13:38:10 +01:00
Ben Arc eac125a28e Works, although .env not currently supported 2021-06-30 13:08:11 +01:00
ton a147c43a97 Fix for #238 Minor issues in API documentation of lnurlp extension
https://github.com/lnbits/lnbits/issues/238
2021-06-30 14:03:16 +02:00
Ben Arc 4e43d2d52e theme saved and works 2021-06-30 12:40:58 +01:00
Ben Arc 40f50135bb default classic theme 2021-06-30 11:21:57 +01:00
Ben Arc 66445b228a Fixed bug 2021-06-29 10:00:10 +01:00
Ben Arc 893801b30e Added env var to be checked
Theme options available to users could be limited here
2021-06-29 09:42:46 +01:00
Ben Arc 78605982fc Working, but need to store selection in localstorage 2021-06-29 09:39:12 +01:00
Ben Arc b0c0b9f851 Changed primary/secondary colours to branded 2021-06-29 02:10:30 +01:00
Ben Arc c39dfbaee2 Kinda working test 2021-06-29 01:19:35 +01:00
Ben Arc 8f441f32b5 added envs, testing color switch 2021-06-28 22:57:23 +01:00
Fitti f88c622f94 Change charge description 2021-06-28 20:19:54 +02:00
Fitti 56b85ecdac Get entire flow working 2021-06-28 20:09:27 +02:00
Fitti ef0dee1c5c Add fiat calculation to donation 2021-06-28 19:12:15 +02:00
Fitti d9f631d3ec Get donation form to load 2021-06-28 19:02:47 +02:00
Fitti 841da76a8c Get working index.html 2021-06-28 18:50:56 +02:00
Fitti d24980aa3c Small fixes + renames 2021-06-28 18:18:52 +02:00
Fitti 39844b257b Further customize copied code 2021-06-28 17:18:35 +02:00
Ben Arc c4fd7f0338 fixed a link issue 2021-06-28 12:40:20 +01:00
Ben Arc 2fb4ce8df4 Check dbs exist before creating 2021-06-28 11:22:17 +01:00
Fitti 3f353a6eae Add donation getter endpoint 2021-06-28 09:06:13 +02:00
Fitti f70eac2a48 Copy and start to modify templates 2021-06-28 09:05:49 +02:00
Fitti 22a68f3e9f Redirect to correct Twitch stream 2021-06-24 10:20:57 +02:00
Fitti 1ed23c2635 Fix not awaiting get_service 2021-06-24 10:20:38 +02:00
Fitti 775444cd6b Finish donation posting API flow 2021-06-24 09:59:11 +02:00
ArcandGitHub 9a23fa509d Merge pull request #183 from lnbits/StreamerCopilot
StreamerCopilot LN powered streaming!
2021-06-24 01:53:04 +01:00
Ben Arc a78d2b1322 deleted prints 2021-06-24 01:49:39 +01:00
Ben Arc 06f05c75bb Merge branch 'master' into StreamerCopilot 2021-06-24 01:42:37 +01:00
Ben Arc 6dec5c2f6a black 2021-06-24 01:42:23 +01:00
Ben Arc c71fe13134 Merge branch 'master' into StreamerCopilot 2021-06-24 01:40:42 +01:00
Ben Arc e51ef5b722 prettier 2021-06-24 01:40:24 +01:00
Ben Arc 2e8b2e5ac8 Merge branch 'master' into StreamerCopilot 2021-06-24 01:32:26 +01:00
Ben Arc 9becfa0bad black 2021-06-24 01:26:49 +01:00
Ben Arc bd1f411312 Added comment to queue 2021-06-24 01:23:35 +01:00
Ben Arc 95f752fd6a added comment 2021-06-24 01:17:05 +01:00
Ben Arc 3a48a6ca39 payment bug 2021-06-24 00:51:26 +01:00
Ben Arc 137f8a7519 few bugs 2021-06-24 00:33:49 +01:00
Ben Arc 2bc3fb1b91 few bugs 2021-06-23 23:58:39 +01:00
Ben Arc e7824e0e8d prettier 2021-06-23 22:34:03 +01:00
Ben Arc 9267aba6d9 Used prettier 2021-06-23 22:31:44 +01:00
Ben Arc 12c9de7179 Merge remote-tracking branch 'origin/master' 2021-06-23 21:52:11 +01:00
Ben Arc d2cad060fd fixed tickets not showing 2021-06-23 21:51:57 +01:00
fiatjaf 6f88973f75 fail longpolling endpoint after 45 seconds.
this should fix a possible bug in which connections are left open forever or worse.
2021-06-23 17:21:53 -03:00
fiatjaf 1b433f9cbc remove secure headers. 2021-06-23 17:21:53 -03:00
Ben Arc 1ef1b2f4fa Fixed tickets not showing 2021-06-23 20:41:56 +01:00
Ben Arc c349ad43cd Merge branch 'master' into StreamerCopilot 2021-06-23 20:29:10 +01:00
Ben Arc 45738beb59 few text tweaks 2021-06-23 20:28:01 +01:00
Ben Arc a0881779c8 changed desc 2021-06-23 20:25:41 +01:00
Ben Arc dde7b23fbe Merge branch 'master' into StreamerCopilot 2021-06-23 20:22:20 +01:00
Ben Arc 4d6e49cf0d made descriptions small for extensions 2021-06-23 20:21:27 +01:00
Ben Arc fe94cafa4d update link broken 2021-06-23 20:05:20 +01:00
Ben Arc 1f03039fce animation queue working 2021-06-23 17:21:09 +01:00
Ben Arc 22fe89a5f9 changing to animation queue 2021-06-23 14:05:10 +01:00
Ben Arc 9ce7cb4b05 websocket working for rooms 2021-06-23 11:14:06 +01:00
Ben Arc 9f59784bf1 adding better websockets 2021-06-23 09:20:32 +01:00
Ben Arc 9961551dac Merge branch 'master' into StreamerCopilot 2021-06-23 09:08:37 +01:00
Fitti 1cd1a99944 Prevent brute-force token overwriting 2021-06-23 09:53:10 +02:00
Fitti c754017e9d Fix authentication process 2021-06-22 18:29:01 +02:00
Fitti 8386facbdb Add state for authentication 2021-06-22 18:05:07 +02:00
Fitti d16eae2d9d Return json instead of plain text from API 2021-06-22 17:12:55 +02:00
Fitti 8342e2bf8b Return BAD_REQUEST for bad api_post_donation requests 2021-06-22 17:07:44 +02:00
Fitti 9390a79cca Validate api_post_donation requests 2021-06-22 17:06:56 +02:00
Fitti 010d59caac Make class names singular 2021-06-22 16:42:50 +02:00
Fitti 7066f4d11a Make extension directory lowercase 2021-06-22 15:54:34 +02:00
Fitti 76438f5563 Make CamelCase lowercase 2021-06-22 15:46:46 +02:00
Fitti 9c2c04dad2 Fix return type for post_donation 2021-06-22 13:24:11 +02:00
Fitti 74af2c0b02 Expand API endpoints 2021-06-22 13:19:45 +02:00
Fitti 3db23d954b Create various helper functions 2021-06-22 13:19:22 +02:00
Fitti c759f3617e Add models and migrations 2021-06-22 13:18:50 +02:00
Fitti 2dded1b585 Get basic working endpoints
This is a setup that works specifically with a
hardcoded test wallet
2021-06-22 09:55:58 +02:00
ArcandGitHub 0a884e8983 Merge pull request #228 from evildido/fix-concurrent-withdraw
Fix concurrent withdrawal behavior
2021-06-21 23:25:30 +01:00
Fittiandfiatjaf fbda278e42 Fix typos and make small improvements
Credit to Vinnie64 for pointing out one of the errors
2021-06-21 13:53:07 -03:00
fiatjaf 688149fade add trio typing. 2021-06-20 21:17:32 -03:00
fiatjafandGitHub 8dc25fbe50 Merge pull request #220 from talvasconcelos/fix/issue#213 2021-06-20 21:11:58 -03:00
Fittiandfiatjaf 870a61abe9 Add missing instruction to dev installation guide
The missing data folder will throw an exception:
`sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) unable to open database file`
2021-06-20 20:57:12 -03:00
evildido 716ba5760f Fix UPDATE in the calback url 2021-06-20 10:42:49 +02:00
Fitti da893ccba6 Fix missing template 2021-06-20 07:51:24 +02:00
Fitti 420a4cd524 Make extenstion available to load 2021-06-20 07:50:58 +02:00
Fitti fd8d4d3e67 Add TwitchAlerts plugin 2021-06-20 06:34:01 +02:00
Fittiandfiatjaf 8472efb1b5 Fix codeblock in guide 2021-06-19 13:34:51 -03:00
fiatjaf f875dc7b63 "unit" optional on invoice creation, defaults to "sat" 2021-06-19 09:12:16 -03:00
Tiago vasconcelos e2b18eb5a2 prettier fix? 2021-06-18 17:10:27 +01:00
Tiago vasconcelos 8d7dae5ba7 removed some unused methods 2021-06-18 10:12:54 +01:00
Tiago vasconcelos 26cf51c11c removed polling and added listener behaviour 2021-06-18 10:06:45 +01:00
Tiago vasconcelos d8be362ac4 exclude package-lock.json form github 2021-06-18 10:06:16 +01:00
fiatjaf 64c4b97ec9 catch error on redeem_lnurl_withdraw call. 2021-06-17 13:26:09 -03:00
Tiago vasconcelos ff965af11c removed paid column from showing
changed success message, on payment
2021-06-16 12:14:04 +01:00
Tiago vasconcelos b490d9f260 fixed updating paid tickets on index.html 2021-06-16 11:23:58 +01:00
Tiago vasconcelos bb1253f30e - added a refresh button for tickets 2021-06-15 12:46:27 +01:00
Tiago vasconcelos 1667e60464 changed polling to invoice listener
- fixed issue #213
2021-06-15 12:32:23 +01:00
Tiago vasconcelosandfiatjaf d91dbbcac3 allow invoices to be generated using Fiat values 2021-06-14 11:13:49 -03:00
Tiago vasconcelosandfiatjaf cfbd6fca3a Updated readme for splitpayments extension 2021-06-14 11:13:49 -03:00
Tiago vasconcelosandfiatjaf e7209d2883 Fixed issue deleting lnurlp 2021-06-14 08:51:07 -03:00
Tiago vasconcelos 2da68bf974 Update for jukebox extension readme
removed the profit column showing 0
2021-06-14 12:42:28 +01:00
Fittiandfiatjaf 1e0367a451 Fixes #212
This is a quick and dirty fix of issue #212.  
Partially reverts previous changes from commit 5fbd1f08c4
Re-implements the change in a slightly different way.
2021-06-14 05:01:34 -03:00
Ben Arc 0d3cc971a5 Increased polling time 2021-06-13 15:00:06 +01:00
Ben Arc 8fd5a19baf Changed key to adminkey 2021-06-13 14:19:44 +01:00
ArcandGitHub 0d9f9508ac Merge pull request #203 from lnbits/Jukebox
Spotify Jukebox Extension
2021-06-13 14:18:56 +01:00
Ben Arc daa80c90e9 Improved endpoint 2021-06-13 14:01:29 +01:00
Ben Arc e2c45b9256 Merge branch 'master' into Jukebox 2021-06-13 13:04:33 +01:00
Ben Arc 778926a714 updated for black 2021-06-13 13:04:18 +01:00
Ben Arc ed20c75fd0 added api docs 2021-06-13 12:58:46 +01:00
oriolpontandfiatjaf 37d9e30362 Change minWithdrawable from 1 msat to 1000 msats for the lnurl_full_withdraw
1 msat is fractional and thus unacceptable anyway. Also, when withdrawing from lnbits itself, this interferes with the input form to expect fractional 1.001 steps
2021-06-12 12:32:40 -03:00
Ben Arc 9cb6068dd8 Tidied 2021-06-11 20:34:55 +01:00
Ben Arc 78ad57f99f Fixed device search 2021-06-11 20:27:13 +01:00
Ben Arc 10a5db403a Some more checks 2021-06-11 15:45:48 +01:00
Ben Arc 7d615db8e2 added some checks to make sure the jukebox is connected 2021-06-11 15:42:50 +01:00
Ben Arc ff80f3d585 Merge branch 'master' into Jukebox 2021-06-10 23:33:26 +01:00
Ben Arc 8500b7767c Fixed button bug 2021-06-10 23:09:24 +01:00
fiatjaf 7ab4553ef5 splitpayments extension. 2021-06-10 12:19:50 -03:00
Tiago vasconcelosandfiatjaf 239e0cdbcf button default on statspay default to back to merchant 2021-06-10 12:03:44 -03:00
Tiago vasconcelosandfiatjaf 233241ba64 fix allowing decimals on Offlineshop extension 2021-06-10 12:03:44 -03:00
Tiago vasconcelosandfiatjaf 65a85e3612 fix show commit version on UI 2021-06-10 12:03:44 -03:00
Tiago vasconcelosandfiatjaf 5dacf52bdb sats pay server documentation 2021-06-10 12:03:44 -03:00
Tiago vasconcelosandfiatjaf 5f888ce279 watch-only doc update 2021-06-10 12:03:44 -03:00
TSandfiatjaf 558f3acfcb changes configuration of environment for Dockerfile and adds the installation procedure to the documentation 2021-06-09 12:09:31 -03:00
2e8145faac Documentation Updates (#202)
Updates for user manager and subdomains extensions

Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-09 12:07:42 -03:00
keblekandfiatjaf a1e12fcd0e Adding clarity for LNPay webhooks 2021-06-09 12:07:08 -03:00
Ben Arc 2fb472db19 Improved timings of alerts 2021-06-08 00:13:17 +01:00
Ben Arc ed6cccbf0b refreshes currently playing 2021-06-07 23:36:11 +01:00
Ben Arc cb6c3629ea Works! 2021-06-07 23:29:40 +01:00
6a59208297 Fix form reset on LNURLp (#201)
Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-07 18:54:22 -03:00
Ben Arc ed60b7f6ee broke 2021-06-07 15:42:02 +01:00
Ben Arc 430cbb8275 Added backend websocket and queue maker 2021-06-07 14:44:50 +01:00
Ben Arc 9038bc4556 Breaking, adding queue update 2021-06-07 14:24:04 +01:00
Ben Arc 9963412fe6 time to add a websocket 2021-06-07 12:03:13 +01:00
Ben Arc 3faf426945 Merge remote-tracking branch 'origin/master' into Jukebox 2021-06-06 22:14:36 +01:00
fiatjaf 5fbd1f08c4 lnurlp: only send success_action when it exists. 2021-06-05 15:20:36 -03:00
7412373f11 Livestream Fix (#198)
Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-04 16:27:25 -03:00
keblekandfiatjaf f4c3ac18b6 Adding comment for LNPay
Adding what they are called in the website to add clarity
2021-06-04 16:26:39 -03:00
fiatjaf 05139b3305 lnurlp: fix success_url checking. 2021-06-04 00:29:55 -03:00
d9b37c077d More docs (#197)
Paywall and DJ Livestream updated

Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-03 22:56:50 -03:00
fiatjaf fc792f874b public api for checking an invoice status with longpolling. 2021-06-02 17:05:07 -03:00
7a25c8ce24 Docs (#196)
Updated docs for LNURL pay, LNURL withdraw and Offline shop

Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-02 14:16:03 -03:00
benarc 56d1be1cda listener not working 2021-06-01 23:40:50 +01:00
ebf3dfd076 New Docs (#195)
New docs for TPOS and Events, and a fix in Events extension!

Co-authored-by: Tiago Vasconcelos <tvasconcelos@gmail.com>
2021-06-01 19:25:57 -03:00
benarc 2996a84b48 Little bug 2021-06-01 21:21:01 +01:00
benarc ba7bd91140 removed display queue because spotify doesn't support 2021-06-01 21:19:41 +01:00
fiatjaf b06e1c8fa9 try-catch in pay_invoice on global lnurl-withdraw. 2021-06-01 13:28:05 -03:00
benarc 5052826bc2 Added track update, still needs current track 2021-06-01 16:50:06 +01:00
Ben Arc 3a8a8f95e9 Merge branch 'master' into Jukebox 2021-05-31 09:13:30 +01:00
Ben Arc 8b9d99ab2d Added better description for connecting account 2021-05-31 09:13:09 +01:00
Ben Arc b6e6d942e4 Added api info 2021-05-29 14:03:03 +01:00
Ben Arc 2dfd3f0371 Merge branch 'usermanagerupdate' 2021-05-29 13:43:21 +01:00
Ben Arc 8af88022cd Updated usermanager
Made use of the email and password fields in db
2021-05-29 13:42:14 +01:00
Pacandfiatjaf 036e45d77c Enforce https on Success URL for lnurlp 2021-05-28 10:01:57 -03:00
fiatjaf f6a8e96087 fix note about global virtualenv. 2021-05-22 20:35:37 -03:00
fiatjaf b9db504f34 fix links to extensions in payments tags. 2021-05-21 22:57:22 -03:00
fiatjaf 9ba5342f3c subdomain: guard allowed_record_types type. 2021-05-15 11:06:35 -03:00
fiatjaf 5dbbca0ffb improve invoice deleting code. 2021-05-14 13:13:42 -03:00
Ben Arc f43c227b95 Creates invoice 2021-05-12 15:47:21 +01:00
Ben Arc e726d752d8 Damn, spotify json broken :( 2021-05-10 20:29:26 +01:00
fiatjaf 8cff11bf72 global quart errorhandler. 2021-05-06 23:22:02 -03:00
fiatjaf 2b21a4f479 g.nursery -> current_app.nursery 2021-05-06 12:41:44 -03:00
Ben Arc 2a4457afec Fixed a few bugs, started invoice endpoint 2021-05-06 10:02:18 +01:00
fiatjaf d5f5c9473f guard against amount=NaN in two other lnurlw contexts. 2021-05-05 20:45:49 -03:00
Ben Arc 396de3cfa3 Added jukebox page 2021-05-05 23:03:22 +01:00
Ben Arc a698ba7a26 Adding endpoints for jukebox 2021-05-05 19:20:53 +01:00
Ben Arc bfdff79dd5 Added shareable qr, broke jukebox creation 2021-05-05 15:58:33 +01:00
Ben Arc be234c349f Added update jukebox 2021-05-03 23:22:40 +01:00
Dave ScoteseandGitHub 9b793c9f3a Nearly undetectable typo fix. (#188)
Regularily => regularly
2021-05-02 23:21:27 -03:00
Ben Arc 245a819f19 Table working 2021-04-29 22:38:57 +01:00
Ben Arc 38c2270abf black + added devices 2021-04-29 08:47:58 +01:00
Ben Arc b594281f83 Can connect to spotify ccount 2021-04-29 00:40:58 +01:00
Ben Arc dc10a0f52b Connects to spotify 2021-04-28 12:04:47 +01:00
fiatjaf b6f015b561 lnurlp: account for invalid amount in querystring. 2021-04-27 15:52:44 -03:00
supertestnetandGitHub 88895f80be note on virtualenv installation (#185) 2021-04-27 11:22:14 -03:00
Ben Arc 3f8890def7 Fixed few bugs 🎵 2021-04-27 10:13:04 +01:00
Ben Arc c0b18d78cc Initial commit 🎵 2021-04-27 10:07:17 +01:00
Ben Arc 00b8a52421 Merge branch 'master' into StreamerCopilot 2021-04-27 09:13:14 +01:00
Ben Arc 53c76f5968 removed key 2021-04-22 09:32:08 +01:00
Ben Arc 71a6d90283 Changed permission for deleting, 2021-04-21 16:00:11 +01:00
Ben Arc 39561efe38 Merge branch 'master' into StreamerCopilot 2021-04-21 14:03:03 +01:00
Ben Arc 1e37b9cb55 Merge branch 'master' into StreamerCopilot 2021-04-21 13:41:11 +01:00
Ben Arc b25dc51563 Merge branch 'master' into StreamerCopilot 2021-04-21 13:35:06 +01:00
Ben Arc 32c822b837 Merge branch 'master' into StreamerCopilot 2021-04-21 13:12:28 +01:00
Ben Arc 8654fa37da made description smaller 2021-04-21 10:28:26 +01:00
Ben Arc f8e8be23ab Black 2021-04-20 21:51:53 +01:00
Ben Arc ef50b5575a bool bug 2021-04-20 19:44:16 +01:00
Ben Arc 4af48b4032 bool bug 2021-04-20 19:40:12 +01:00
Ben Arc 8f9c98a1e6 fixed price check bool 2021-04-20 19:38:43 +01:00
Ben Arc c61b7f1c41 yet another style tweak 2021-04-20 19:32:34 +01:00
Ben Arc 5c921039c5 Little qr tweak 2021-04-20 19:27:56 +01:00
Ben Arc b3860d9889 qr too big 2021-04-20 19:10:18 +01:00
Ben Arc 343049dbe4 Moved qr down a bit to allow room for jitsi thumb 2021-04-20 19:05:19 +01:00
Ben Arc e198e52756 Changed size lnurl 2021-04-20 19:01:06 +01:00
Ben Arc 7bfbb16f48 message bug 2021-04-20 17:56:12 +01:00
Ben Arc 20747b9d3b equal of higher than threshold 2021-04-20 17:47:59 +01:00
Ben Arc 83c4f619ae Couple little bugs 2021-04-20 17:43:33 +01:00
Ben Arc 361a87110b Fixed couple of display bugs 2021-04-20 17:26:38 +01:00
Ben Arc b6500b9e82 tweaked sizes 2021-04-20 15:49:19 +01:00
Ben Arc e2c3cecdf4 tweaked sizes gifs 2021-04-20 15:45:54 +01:00
Ben Arc d773eb84cf Fixed layout 2021-04-20 15:42:24 +01:00
Ben Arc 8d6253bf5d Changed gif sizes 2021-04-20 15:38:36 +01:00
Ben Arc aaf39ec9f6 updated postioning 2021-04-20 15:32:43 +01:00
Ben Arc 2b89b3a61d updated compose window size 2021-04-20 15:30:18 +01:00
Ben Arc 2a934c4d87 added better sizes to popus 2021-04-20 15:27:23 +01:00
Ben Arc 16119fd5fa changed size panel popup 2021-04-20 15:22:08 +01:00
Ben Arc 607af43b1b panel compose windows 2021-04-20 15:19:10 +01:00
Ben Arc 3ec670785e lnurl to ls 2021-04-20 15:13:29 +01:00
Ben Arc 98ceb551c7 refactor to use local 2021-04-20 15:09:27 +01:00
Ben Arc 7d85310f30 added prop to panel 2021-04-20 14:57:59 +01:00
Ben Arc 459e7cb523 fixed ls 2021-04-20 14:53:52 +01:00
Ben Arc cf96a36b0c another ls test 2021-04-20 14:47:37 +01:00
Ben Arc 5501eb1b74 another test 2021-04-20 14:44:36 +01:00
Ben Arc 5fb0b55fef bug test 2021-04-20 14:42:06 +01:00
Ben Arc af3f6edf05 ls bug 2021-04-20 14:39:21 +01:00
Ben Arc 693e1f9eb7 localstorage paneltest 2021-04-20 14:36:59 +01:00
Ben Arc 1736198a7d localstorage paneltest 2021-04-20 14:33:41 +01:00
Ben Arc 8f3a8e4880 video size bug 2021-04-20 14:07:27 +01:00
Ben Arc 8f666427b6 screen share tweaks 2021-04-20 14:05:08 +01:00
Ben Arc 482c192b6a buggy 2021-04-20 13:53:20 +01:00
Ben Arc 959e717da6 price fetch bug 2021-04-20 13:51:07 +01:00
Ben Arc 9b1430a119 launch price check on start 2021-04-20 13:47:48 +01:00
Ben Arc ddeb3fa9b2 Change boot to launching 2021-04-20 13:44:10 +01:00
Ben Arc 92722b315b replaced iframe with share screen 2021-04-20 13:41:59 +01:00
Ben Arc 02a406d0fa disabled change webcam size 2021-04-20 13:32:26 +01:00
Ben Arc a001b9ab28 moved messages to top left 2021-04-20 12:42:58 +01:00
Ben Arc 6dba793f71 fixed color messages 2021-04-20 12:38:50 +01:00
Ben Arc 37d0f4947e random colors for messages 2021-04-20 12:36:44 +01:00
Ben Arc 2803f83ffb messages broken 2021-04-20 12:31:23 +01:00
Ben Arc 93c1cf4867 fixed init url 2021-04-20 12:21:15 +01:00
Ben Arc 469d7809b4 fixed copilot get 2021-04-20 12:16:36 +01:00
Ben Arc fe6e6764fa Merge remote-tracking branch 'origin/master' into StreamerCopilot 2021-04-20 10:48:43 +01:00
Ben Arc 8422dadcc5 added docs for ws api point 2021-04-20 10:45:54 +01:00
Ben Arc 25c2d2c7c6 create an initialising event fro compose 2021-04-20 10:34:48 +01:00
Ben Arc b520d0d8b2 included some checks for the thresholds 2021-04-20 09:35:32 +01:00
Ben Arc db5b3306cd Updated migrations 2021-04-20 09:28:06 +01:00
Ben Arc 18752eeda9 message too long 2021-04-20 09:20:50 +01:00
Ben Arc 4577c497c1 moved lnurl-pay message checkbox 2021-04-20 09:18:40 +01:00
Ben Arc 443925afa1 Added random colour for messages 2021-04-20 08:54:45 +01:00
Ben Arc 0946bcc1b1 Working! 2021-04-20 08:50:53 +01:00
Ben Arc dd95f3e40f chnaged message font 2021-04-20 00:44:17 +01:00
Ben Arc 5a306a53e4 Changed font 2021-04-20 00:41:06 +01:00
Ben Arc 8b52dfc307 added updater function 2021-04-20 00:37:51 +01:00
Ben Arc bde9a4a2ec added shortuuid 2021-04-20 00:12:44 +01:00
Ben Arc c397d4c3c4 changed amount 2021-04-20 00:08:55 +01:00
Ben Arc 87e0e7d1d9 little bug 2021-04-19 23:58:05 +01:00
Ben Arc 10f6ee7af1 fixed bug 2021-04-19 23:56:52 +01:00
Ben Arc 651ae2a386 Added listener 2021-04-19 23:45:40 +01:00
Ben Arc 05317eb5d6 payment webhook set to false exernal 2021-04-19 09:50:42 +01:00
Ben Arc a0001a015a added message notification 2021-04-19 09:48:05 +01:00
Ben Arc ab81fb08b0 fixed payment threshold webhook 2021-04-19 09:40:56 +01:00
Ben Arc 77860ec16d Forgot to add keyword 2021-04-19 09:35:26 +01:00
Ben Arc 874e0773c0 added webhook and messages 2021-04-19 09:32:41 +01:00
Ben Arc 5dad118991 black 2021-04-19 08:42:08 +01:00
Ben Arc b500a02f02 black 2021-04-16 22:22:36 +01:00
Ben Arc dfc8dfc426 adrbadsf 2021-04-16 22:17:54 +01:00
Ben Arc ee0fab0087 finally 2021-04-16 22:13:00 +01:00
Ben Arc b051f7776c s 2021-04-16 22:09:56 +01:00
Ben Arc 1349f74c45 tests 2021-04-16 22:07:06 +01:00
Ben Arc 88e11b4793 dasv 2021-04-16 22:02:22 +01:00
Ben Arc 3acfaf6fbd dwasdv 2021-04-16 21:59:45 +01:00
Ben Arc 8283254865 bitches 2021-04-16 21:51:47 +01:00
Ben Arc 3c6c3b26fe l 2021-04-16 21:44:20 +01:00
Ben Arc e55eb4679b r 2021-04-16 21:42:16 +01:00
Ben Arc a03033a851 u 2021-04-16 21:41:24 +01:00
Ben Arc ad7bba60d3 n 2021-04-16 21:37:40 +01:00
Ben Arc 176406a982 l 2021-04-16 21:35:30 +01:00
Ben Arc 211cdd7882 q 2021-04-16 21:33:47 +01:00
Ben Arc f65d04a4d8 d 2021-04-16 21:25:27 +01:00
Ben Arc 13e771eb9a :( 2021-04-16 21:23:13 +01:00
Ben Arc 86353a61b6 s 2021-04-16 21:19:28 +01:00
Ben Arc 2007c00c8e prints 2021-04-16 21:17:30 +01:00
Ben Arc f8603ed643 metadata change 2021-04-16 21:16:15 +01:00
Ben Arc 271f7bdac5 str 2021-04-16 21:05:30 +01:00
Ben Arc ace8aa4127 s 2021-04-16 21:04:48 +01:00
Ben Arc 95e6a8685d cp_id 2021-04-16 21:03:48 +01:00
Ben Arc c469bcff27 raw link 2021-04-16 21:02:55 +01:00
Ben Arc 4a15dfc667 bah 2021-04-16 20:59:56 +01:00
Ben Arc 1d27f88b96 print test 2021-04-16 20:55:47 +01:00
Ben Arc 37c9b9e825 print tests 2021-04-16 20:50:50 +01:00
Ben Arc 48961d89de lnurl 2021-04-16 20:44:08 +01:00
Ben Arc 7b4bf3cdc0 Lnurl all-father, please let this work 2021-04-16 20:42:37 +01:00
Ben Arc 9ec3fea73c lnurl failing 2021-04-16 13:28:40 +01:00
Ben Arc f8920fe8ed bug fix 2021-04-16 13:19:57 +01:00
Ben Arc eba8312743 bug fix 2021-04-16 13:18:43 +01:00
Ben Arc ad18031b8b lnurl bug fix 2021-04-16 13:17:21 +01:00
Ben Arc c3df979a67 changed lnurl 2021-04-16 13:09:46 +01:00
Ben Arc 8ace4d4c96 Try delivering json directly 2021-04-16 13:07:53 +01:00
Ben Arc a8cc8241e6 json fix 2021-04-16 12:24:00 +01:00
Ben Arc 79b046ede1 json fix 2021-04-16 12:22:22 +01:00
Ben Arc d083b29d79 black 2021-04-16 12:20:05 +01:00
Ben Arc f8745cadf0 more tests 2021-04-16 12:07:47 +01:00
Ben Arc 74eb91d63f some tests for my lnurl 2021-04-16 12:05:07 +01:00
Ben Arc cd3c6394f2 unknown bug in my lnurl :( 2021-04-16 11:44:07 +01:00
Ben Arc dea61c23be removed success action 2021-04-16 11:37:17 +01:00
Ben Arc 9b5d4ac7b5 fixed if 2021-04-16 11:34:21 +01:00
Ben Arc 3504c568d1 allowed for milisat 2021-04-16 11:31:31 +01:00
Ben Arc 598f578f4d added lnurl json 2021-04-16 11:28:50 +01:00
Ben Arc 1c81f3b2a5 Added ack and price 2021-04-16 11:07:14 +01:00
Ben Arc feefc465e8 Fixed lnurl_response 2021-04-16 09:18:24 +01:00
Ben Arc fa0af46ed7 toggle check 2021-04-15 09:49:07 +01:00
Ben Arc f46e992bda Stupid string bug 2021-04-15 09:46:37 +01:00
Ben Arc 704fc042cb Should be working
Need to debug lnurl logic, but apart from that I think its done
2021-04-15 09:41:37 +01:00
Ben Arc 1af84b2b3d Should be working
Need to check lnurl logic, and debug
2021-04-15 09:39:27 +01:00
Ben Arc 34d659f9f3 Merge remote-tracking branch 'origin/StreamerCopilot' into StreamerCopilot 2021-04-14 23:46:07 +01:00
Ben Arc a6a917fb5e Very hacky checker for websockets
Looking pretty fucking dope 


Added some extra gifs


Added wallet, lnurl generation should work


Damn you lnurl


check https for wss


added z for lnurlp


zindex to the moon


zindex to moon


lnurl too big in ambition


lnurl big


copy/paste bug address


attach lnurl to view


please work


lnurl bug


lnurl bug


bigger lnurl


added reconect button


me bug


await a string
2021-04-14 23:45:28 +01:00
Ben Arc 650fa35905 await a string 2021-04-14 23:35:24 +01:00
Ben Arc 806e01cb2e me bug 2021-04-14 23:32:21 +01:00
Ben Arc 812f94879f added reconect button 2021-04-14 23:28:08 +01:00
Ben Arc bd799ba959 bigger lnurl 2021-04-14 23:25:11 +01:00
Ben Arc 0316cf740e lnurl bug 2021-04-14 23:23:03 +01:00
Ben Arc 7d3ba300e4 lnurl bug 2021-04-14 23:18:27 +01:00
Ben Arc 19a0303caa please work 2021-04-14 23:15:09 +01:00
Ben Arc 390ee227ec attach lnurl to view 2021-04-14 23:01:39 +01:00
Ben Arc a4255e1594 copy/paste bug address 2021-04-14 22:44:08 +01:00
Ben Arc c39399befa lnurl big 2021-04-14 22:35:12 +01:00
Ben Arc d288497d9d lnurl too big in ambition 2021-04-14 22:32:05 +01:00
Ben Arc 859836811e zindex to moon 2021-04-14 22:30:24 +01:00
Ben Arc 12adfabbb0 zindex to the moon 2021-04-14 22:24:18 +01:00
Ben Arc 082bcc525e added z for lnurlp 2021-04-14 22:21:13 +01:00
Ben Arc 589a482faa check https for wss 2021-04-14 22:09:33 +01:00
Ben Arc 412ac8d110 Damn you lnurl 2021-04-14 22:03:36 +01:00
Ben Arc 0e67e6a0bb Added wallet, lnurl generation should work 2021-04-14 21:39:51 +01:00
Ben Arc af2e1b5d02 Added some extra gifs 2021-04-14 20:18:06 +01:00
Ben Arc dea442cbcc Looking pretty fucking dope 2021-04-14 18:11:33 +01:00
Ben Arc 6dceb61ba8 Very hacky checker for websockets 2021-04-14 17:00:18 +01:00
Ben Arc 2a3e1c7f8a websocket test 2021-04-14 09:34:14 +01:00
Ben Arc 4682fd678c secure wss 2021-04-14 09:32:51 +01:00
Ben Arc f5487b2185 Websocket test 2021-04-14 09:29:25 +01:00
Ben Arc 2ac9e75dac Adding websockets for panel (trying at least) 2021-04-14 09:21:20 +01:00
Ben Arc 58517fba53 video stream works, added gif 2021-04-13 22:12:23 +01:00
Ben Arc 167247bdd6 Merge branch 'master' into StreamerCopilot 2021-04-13 21:43:07 +01:00
Ben Arc 6a05fd7106 adding camera 2021-04-13 21:22:51 +01:00
Ben Arc 80969fbd9b Control panel looking pretty good 2021-04-13 20:15:33 +01:00
Ben Arc 0305132fc3 Merge branch 'master' into StreamerCopilot 2021-04-13 19:20:45 +01:00
Ben Arc 9cd3e00b83 Started working on front pages 2021-04-13 19:20:12 +01:00
Ben Arc df5e4cb9db Added frontend pages and started making lnurl 2021-04-13 17:10:25 +01:00
Ben Arc 9ec208c434 Changed streamercopilot name 2021-04-13 14:53:41 +01:00
Ben Arc 167c31f778 Changed so user can use multiple animations 2021-04-12 22:05:54 +01:00
Ben Arc e4c893c1f2 updated API docs 2021-04-12 20:42:44 +01:00
Ben Arc f29a852e9d Started streamer copilot extension 2021-04-12 20:31:39 +01:00
400 changed files with 48589 additions and 8555 deletions
+24 -6
View File
@@ -5,14 +5,30 @@ QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
LNBITS_SITE_TITLE=LNbits
LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="."
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://...
# for both PostgreSQL and CockroachDB, you'll need to install
# psycopg2 as an additional dependency
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
# disable selected extensions, or use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"
# Change theme
LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
@@ -35,20 +51,22 @@ LNBITS_KEY=LNBITS_ADMIN_KEY
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"
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="HEXSTRING"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# LNPayWallet
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
# Secret API Key under developers tab
LNPAY_API_KEY=LNPAY_API_KEY
# Wallet Admin in Wallet Access Keys
LNPAY_WALLET_KEY=LNPAY_ADMIN_KEY
# LntxbotWallet
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/
LNTXBOT_API_ENDPOINT=https://lntxbot.com/
LNTXBOT_KEY=LNTXBOT_ADMIN_KEY
# OpenNodeWallet
+42 -20
View File
@@ -3,11 +3,11 @@ name: tests
on: [push, pull_request]
jobs:
build:
unit:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@@ -15,22 +15,44 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
env:
LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
LNBITS_FORCE_HTTPS: 0
LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/
LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
run: |
pip install pytest pytest-cov
pytest --cov=lnbits --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio requests trio mock
- name: Run tests
run: make test
# build:
# runs-on: ubuntu-latest
# strategy:
# matrix:
# python-version: [3.7, 3.8]
# steps:
# - uses: actions/checkout@v2
# - name: Set up Python ${{ matrix.python-version }}
# uses: actions/setup-python@v1
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r requirements.txt
# - name: Test with pytest
# env:
# LNBITS_BACKEND_WALLET_CLASS: LNPayWallet
# LNBITS_FORCE_HTTPS: 0
# LNPAY_API_ENDPOINT: https://api.lnpay.co/v1/
# LNPAY_API_KEY: sak_gG5pSFZhFgOLHm26a8hcWvXKt98yd
# LNPAY_ADMIN_KEY: waka_HqWfOoNE0TPqmQHSYErbF4n9
# LNPAY_INVOICE_KEY: waki_ZqFEbhrTyopuPlOZButZUw
# LNPAY_READ_KEY: wakr_6IyTaNrvSeu3jbojSWt4ou6h
# run: |
# pip install pytest pytest-cov
# pytest --cov=lnbits --cov-report=xml
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1
# with:
# file: ./coverage.xml
+2
View File
@@ -6,6 +6,7 @@ __pycache__
*$py.class
.mypy_cache
.vscode
*-lock.json
*.egg
*.egg-info
@@ -14,6 +15,7 @@ __pycache__
.webassets-cache
htmlcov
test-reports
tests/data
*.swo
*.swp
+3 -10
View File
@@ -9,6 +9,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install build deps
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential
RUN python -m pip install --upgrade pip
# Install runtime deps
COPY requirements.txt /tmp/requirements.txt
@@ -18,7 +19,7 @@ RUN pip install -r /tmp/requirements.txt
RUN pip install pylightning
# Install LND specific deps
RUN pip install lndgrpc purerpc
RUN pip install lndgrpc
# Production image
FROM python:3.7-slim as lnbits
@@ -31,18 +32,10 @@ ENV VIRTUAL_ENV="/opt/venv"
COPY --from=builder --chown=1000:1000 $VIRTUAL_ENV $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Setup Quart
ENV QUART_APP="lnbits.app:create_app()"
ENV QUART_ENV="development"
ENV QUART_DEBUG="true"
# App
ENV LNBITS_BIND="0.0.0.0:5000"
# Copy in app source
WORKDIR /app
COPY --chown=1000:1000 lnbits /app/lnbits
EXPOSE 5000
CMD quart assets && quart migrate && hypercorn -k trio --bind $LNBITS_BIND 'lnbits.app:create_app()'
CMD ["uvicorn", "lnbits.__main__:app", "--port", "5000", "--host", "0.0.0.0"]
+9
View File
@@ -1,3 +1,5 @@
.PHONY: test
all: format check requirements.txt
format: prettier black
@@ -26,3 +28,10 @@ Pipfile.lock: Pipfile
requirements.txt: Pipfile.lock
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt
test:
rm -rf ./tests/data
mkdir -p ./tests/data
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
./venv/bin/pytest -s
+8 -8
View File
@@ -14,24 +14,24 @@ environs = "*"
lnurl = "==0.3.6"
pyscss = "*"
shortuuid = "*"
quart = "*"
quart-cors = "*"
quart-compress = "*"
secure = "*"
typing-extensions = "*"
httpx = "*"
quart-trio = "*"
trio = "==0.16.0"
hypercorn = {extras = ["trio"], version = "*"}
sqlalchemy-aio = "*"
embit = "*"
pyqrcode = "*"
pypng = "*"
sqlalchemy = "==1.3.23"
psycopg2-binary = "*"
aiofiles = "*"
asyncio = "*"
fastapi = "*"
uvicorn = {extras = ["standard"], version = "*"}
sse-starlette = "*"
jinja2 = "3.0.1"
pyngrok = "*"
[dev-packages]
black = "==20.8b1"
pytest = "*"
pytest-cov = "*"
mypy = "latest"
pytest-trio = "*"
Generated
+500 -497
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -12,6 +12,8 @@ LNbits
# LNbits v0.3 BETA, free and open-source lightning-network wallet/accounts system
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
@@ -22,7 +24,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily.
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
See [lnbits.org](https://lnbits.org) for more detailed documentation.
+653
View File
@@ -0,0 +1,653 @@
## 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
# Change these values as needed
sqfolder = "data/"
pgdb = "lnbits"
pguser = "postgres"
pgpswd = "yourpassword"
pghost = "localhost"
pgport = "5432"
pgschema = ""
def get_sqlite_cursor(sqdb) -> sqlite3:
consq = sqlite3.connect(sqdb)
return consq.cursor()
def get_postgres_cursor():
conpg = psycopg2.connect(
database=pgdb, user=pguser, password=pgpswd, host=pghost, port=pgport
)
return conpg.cursor()
def check_db_versions(sqdb):
sqlite = get_sqlite_cursor(sqdb)
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
del dblite["lnurlpos"] # wrongly added?
sqlite.close()
postgres = get_postgres_cursor()
postgres.execute("SELECT * FROM public.dbversions;")
dbpost = dict(postgres.fetchall())
for key in dblite.keys():
if dblite[key] != dbpost[key]:
raise Exception(
f"sqlite database version ({dblite[key]}) of {key} doesn't match postgres database version {dbpost[key]}"
)
connection = postgres.connection
postgres.close()
connection.close()
print("Database versions OK, converting")
def fix_id(seq, values):
if not values or len(values) == 0:
return
postgres = get_postgres_cursor()
max_id = values[len(values) - 1][0]
postgres.execute(f"SELECT setval('{seq}', {max_id});")
connection = postgres.connection
postgres.close()
connection.close()
def insert_to_pg(query, data):
if len(data) == 0:
return
cursor = get_postgres_cursor()
connection = cursor.connection
for d in data:
cursor.execute(query, d)
connection.commit()
cursor.close()
connection.close()
def migrate_core(sqlite_db_file):
sq = get_sqlite_cursor(sqlite_db_file)
# ACCOUNTS
res = sq.execute("SELECT * FROM accounts;")
q = f"INSERT INTO public.accounts (id, email, pass) VALUES (%s, %s, %s);"
insert_to_pg(q, res.fetchall())
# WALLETS
res = sq.execute("SELECT * FROM wallets;")
q = f'INSERT INTO public.wallets (id, name, "user", adminkey, inkey) VALUES (%s, %s, %s, %s, %s);'
insert_to_pg(q, res.fetchall())
# API PAYMENTS
res = sq.execute("SELECT * FROM apipayments;")
q = f"""
INSERT INTO public.apipayments(
checking_id, amount, fee, wallet, pending, memo, "time", hash, preimage, bolt11, extra, webhook, webhook_status)
VALUES (%s, %s, %s, %s, %s::boolean, %s, to_timestamp(%s), %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# BALANCE CHECK
res = sq.execute("SELECT * FROM balance_check;")
q = f"INSERT INTO public.balance_check(wallet, service, url) VALUES (%s, %s, %s);"
insert_to_pg(q, res.fetchall())
# BALANCE NOTIFY
res = sq.execute("SELECT * FROM balance_notify;")
q = f"INSERT INTO public.balance_notify(wallet, url) VALUES (%s, %s);"
insert_to_pg(q, res.fetchall())
# EXTENSIONS
res = sq.execute("SELECT * FROM extensions;")
q = f'INSERT INTO public.extensions("user", extension, active) VALUES (%s, %s, %s::boolean);'
insert_to_pg(q, res.fetchall())
print("Migrated: core")
def migrate_ext(sqlite_db_file, schema):
sq = get_sqlite_cursor(sqlite_db_file)
if schema == "bleskomat":
# BLESKOMAT LNURLS
res = sq.execute("SELECT * FROM bleskomat_lnurls;")
q = f"""
INSERT INTO bleskomat.bleskomat_lnurls(
id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# BLESKOMATS
res = sq.execute("SELECT * FROM bleskomats;")
q = f"""
INSERT INTO bleskomat.bleskomats(
id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "captcha":
# CAPTCHA
res = sq.execute("SELECT * FROM captchas;")
q = f"""
INSERT INTO captcha.captchas(
id, wallet, url, memo, description, amount, "time", remembers, extras)
VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s), %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "copilot":
# OLD COPILOTS
res = sq.execute("SELECT * FROM copilots;")
q = f"""
INSERT INTO copilot.copilots(
id, "user", title, lnurl_toggle, wallet, animation1, animation2, animation3, animation1threshold, animation2threshold, animation3threshold, animation1webhook, animation2webhook, animation3webhook, lnurl_title, show_message, show_ack, show_price, amount_made, fullscreen_cam, iframe_url, "timestamp")
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# NEW COPILOTS
q = f"""
INSERT INTO copilot.newer_copilots(
id, "user", title, lnurl_toggle, wallet, animation1, animation2, animation3, animation1threshold, animation2threshold, animation3threshold, animation1webhook, animation2webhook, animation3webhook, lnurl_title, show_message, show_ack, show_price, amount_made, fullscreen_cam, iframe_url, "timestamp")
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "events":
# EVENTS
res = sq.execute("SELECT * FROM events;")
q = f"""
INSERT INTO events.events(
id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold, "time")
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# EVENT TICKETS
res = sq.execute("SELECT * FROM ticket;")
q = f"""
INSERT INTO events.ticket(
id, wallet, event, name, email, registered, paid, "time")
VALUES (%s, %s, %s, %s, %s, %s::boolean, %s::boolean, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "example":
# Example doesn't have a database at the moment
pass
elif schema == "hivemind":
# Hivemind doesn't have a database at the moment
pass
elif schema == "jukebox":
# JUKEBOXES
res = sq.execute("SELECT * FROM jukebox;")
q = f"""
INSERT INTO jukebox.jukebox(
id, "user", title, wallet, inkey, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# JUKEBOX PAYMENTS
res = sq.execute("SELECT * FROM jukebox_payment;")
q = f"""
INSERT INTO jukebox.jukebox_payment(
payment_hash, juke_id, song_id, paid)
VALUES (%s, %s, %s, %s::boolean);
"""
insert_to_pg(q, res.fetchall())
elif schema == "withdraw":
# WITHDRAW LINK
res = sq.execute("SELECT * FROM withdraw_link;")
q = f"""
INSERT INTO withdraw.withdraw_link (
id,
wallet,
title,
min_withdrawable,
max_withdrawable,
uses,
wait_time,
is_unique,
unique_hash,
k1,
open_time,
used,
usescsv
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# WITHDRAW HASH CHECK
res = sq.execute("SELECT * FROM hash_check;")
q = f"""
INSERT INTO withdraw.hash_check (id, lnurl_id)
VALUES (%s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "watchonly":
# WALLETS
res = sq.execute("SELECT * FROM wallets;")
q = f"""
INSERT INTO watchonly.wallets (
id,
"user",
masterpub,
title,
address_no,
balance
)
VALUES (%s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# ADDRESSES
res = sq.execute("SELECT * FROM addresses;")
q = f"""
INSERT INTO watchonly.addresses (id, address, wallet, amount)
VALUES (%s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# MEMPOOL
res = sq.execute("SELECT * FROM mempool;")
q = f"""
INSERT INTO watchonly.mempool ("user", endpoint)
VALUES (%s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "usermanager":
# USERS
res = sq.execute("SELECT * FROM users;")
q = f"""
INSERT INTO usermanager.users (id, name, admin, email, password)
VALUES (%s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# WALLETS
res = sq.execute("SELECT * FROM wallets;")
q = f"""
INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey)
VALUES (%s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "tpos":
# TPOSS
res = sq.execute("SELECT * FROM tposs;")
q = f"""
INSERT INTO tpos.tposs (id, wallet, name, currency)
VALUES (%s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "tipjar":
# TIPJARS
res = sq.execute("SELECT * FROM TipJars;")
q = f"""
INSERT INTO tipjar.TipJars (id, name, wallet, onchain, webhook)
VALUES (%s, %s, %s, %s, %s);
"""
pay_links = res.fetchall()
insert_to_pg(q, pay_links)
fix_id("tipjar.tipjars_id_seq", pay_links)
# TIPS
res = sq.execute("SELECT * FROM Tips;")
q = f"""
INSERT INTO tipjar.Tips (id, wallet, name, message, sats, tipjar)
VALUES (%s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "subdomains":
# DOMAIN
res = sq.execute("SELECT * FROM domain;")
q = f"""
INSERT INTO subdomains.domain (
id,
wallet,
domain,
webhook,
cf_token,
cf_zone_id,
description,
cost,
amountmade,
allowed_record_types,
time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# SUBDOMAIN
res = sq.execute("SELECT * FROM subdomain;")
q = f"""
INSERT INTO subdomains.subdomain (
id,
domain,
email,
subdomain,
ip,
wallet,
sats,
duration,
paid,
record_type,
time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "streamalerts":
# SERVICES
res = sq.execute("SELECT * FROM Services;")
q = f"""
INSERT INTO streamalerts.Services (
id,
state,
twitchuser,
client_id,
client_secret,
wallet,
onchain,
servicename,
authenticated,
token
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean, %s);
"""
services = res.fetchall()
insert_to_pg(q, services)
fix_id("streamalerts.services_id_seq", services)
# DONATIONS
res = sq.execute("SELECT * FROM Donations;")
q = f"""
INSERT INTO streamalerts.Donations (
id,
wallet,
name,
message,
cur_code,
sats,
amount,
service,
posted,
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::boolean);
"""
insert_to_pg(q, res.fetchall())
elif schema == "splitpayments":
# TARGETS
res = sq.execute("SELECT * FROM targets;")
q = f"""
INSERT INTO splitpayments.targets (wallet, source, percent, alias)
VALUES (%s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "satspay":
# CHARGES
res = sq.execute("SELECT * FROM charges;")
q = f"""
INSERT INTO satspay.charges (
id,
"user",
description,
onchainwallet,
onchainaddress,
lnbitswallet,
payment_request,
payment_hash,
webhook,
completelink,
completelinktext,
time,
amount,
balance,
timestamp
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "satsdice":
# SATSDICE PAY
res = sq.execute("SELECT * FROM satsdice_pay;")
q = f"""
INSERT INTO satsdice.satsdice_pay (
id,
wallet,
title,
min_bet,
max_bet,
amount,
served_meta,
served_pr,
multiplier,
haircut,
chance,
base_url,
open_time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# SATSDICE WITHDRAW
res = sq.execute("SELECT * FROM satsdice_withdraw;")
q = f"""
INSERT INTO satsdice.satsdice_withdraw (
id,
satsdice_pay,
value,
unique_hash,
k1,
open_time,
used
)
VALUES (%s, %s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# SATSDICE PAYMENT
res = sq.execute("SELECT * FROM satsdice_payment;")
q = f"""
INSERT INTO satsdice.satsdice_payment (
payment_hash,
satsdice_pay,
value,
paid,
lost
)
VALUES (%s, %s, %s, %s::boolean, %s::boolean);
"""
insert_to_pg(q, res.fetchall())
# SATSDICE HASH CHECK
res = sq.execute("SELECT * FROM hash_checkw;")
q = f"""
INSERT INTO satsdice.hash_checkw (id, lnurl_id)
VALUES (%s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "paywall":
# PAYWALLS
res = sq.execute("SELECT * FROM paywalls;")
q = f"""
INSERT INTO paywall.paywalls(
id,
wallet,
url,
memo,
amount,
time,
remembers,
extra
)
VALUES (%s, %s, %s, %s, %s, to_timestamp(%s), %s, %s);
"""
insert_to_pg(q, res.fetchall())
elif schema == "offlineshop":
# SHOPS
res = sq.execute("SELECT * FROM shops;")
q = f"""
INSERT INTO offlineshop.shops (id, wallet, method, wordlist)
VALUES (%s, %s, %s, %s);
"""
shops = res.fetchall()
insert_to_pg(q, shops)
fix_id("offlineshop.shops_id_seq", shops)
# ITEMS
res = sq.execute("SELECT * FROM items;")
q = f"""
INSERT INTO offlineshop.items (shop, id, name, description, image, enabled, price, unit)
VALUES (%s, %s, %s, %s, %s, %s::boolean, %s, %s);
"""
items = res.fetchall()
insert_to_pg(q, items)
fix_id("offlineshop.items_id_seq", items)
elif schema == "lnurlpos":
# LNURLPOSS
res = sq.execute("SELECT * FROM lnurlposs;")
q = f"""
INSERT INTO lnurlpos.lnurlposs (id, key, title, wallet, currency, timestamp)
VALUES (%s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# LNURLPOS PAYMENT
res = sq.execute("SELECT * FROM lnurlpospayment;")
q = f"""
INSERT INTO lnurlpos.lnurlpospayment (id, posid, payhash, payload, pin, sats, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "lnurlp":
# PAY LINKS
res = sq.execute("SELECT * FROM pay_links;")
q = f"""
INSERT INTO lnurlp.pay_links (
id,
wallet,
description,
min,
served_meta,
served_pr,
webhook_url,
success_text,
success_url,
currency,
comment_chars,
max
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
"""
pay_links = res.fetchall()
insert_to_pg(q, pay_links)
fix_id("lnurlp.pay_links_id_seq", pay_links)
elif schema == "lndhub":
# LndHub doesn't have a database at the moment
pass
elif schema == "lnticket":
# TICKET
res = sq.execute("SELECT * FROM ticket;")
q = f"""
INSERT INTO lnticket.ticket (
id,
form,
email,
ltext,
name,
wallet,
sats,
paid,
time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s::boolean, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# FORM
res = sq.execute("SELECT * FROM form2;")
q = f"""
INSERT INTO lnticket.form2 (
id,
wallet,
name,
webhook,
description,
flatrate,
amount,
amountmade,
time
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "livestream":
# LIVESTREAMS
res = sq.execute("SELECT * FROM livestreams;")
q = f"""
INSERT INTO livestream.livestreams (
id,
wallet,
fee_pct,
current_track
)
VALUES (%s, %s, %s, %s);
"""
livestreams = res.fetchall()
insert_to_pg(q, livestreams)
fix_id("livestream.livestreams_id_seq", livestreams)
# PRODUCERS
res = sq.execute("SELECT * FROM producers;")
q = f"""
INSERT INTO livestream.producers (
livestream,
id,
"user",
wallet,
name
)
VALUES (%s, %s, %s, %s, %s);
"""
producers = res.fetchall()
insert_to_pg(q, producers)
fix_id("livestream.producers_id_seq", producers)
# TRACKS
res = sq.execute("SELECT * FROM tracks;")
q = f"""
INSERT INTO livestream.tracks (
livestream,
id,
download_url,
price_msat,
name,
producer
)
VALUES (%s, %s, %s, %s, %s, %s);
"""
tracks = res.fetchall()
insert_to_pg(q, tracks)
fix_id("livestream.tracks_id_seq", tracks)
else:
print(f"Not implemented: {schema}")
sq.close()
return
print(f"Migrated: {schema}")
sq.close()
check_db_versions("data/database.sqlite3")
migrate_core("data/database.sqlite3")
files = os.listdir(sqfolder)
for file in files:
path = f"data/{file}"
if file.startswith("ext_"):
schema = file.replace("ext_", "").split(".")[0]
print(f"Migrating: {schema}")
migrate_ext(path, schema)
+14
View File
@@ -10,3 +10,17 @@ For developers
==============
Thanks for contributing :)
Tests
=====
This project has unit tests that help prevent regressions. Before you can run the tests, you must install a few dependencies:
```bash
./venv/bin/pip install pytest pytest-asyncio requests trio mock
```
Then to run the tests:
```bash
make test
```
+25 -23
View File
@@ -5,32 +5,27 @@ title: Installation
nav_order: 1
---
Installation
============
# Installation
Download the latest stable release https://github.com/lnbits/lnbits/releases
Application dependencies
------------------------
## Application dependencies
The application uses [Pipenv][pipenv] to manage Python packages.
While in development, you will need to install all dependencies:
```sh
$ pipenv shell
$ pipenv install --dev
```
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`
`pip install -U setuptools`
If you wish to use a version of Python higher than 3.7:
```sh
$ pipenv --python 3.8 install --dev
```
pipenv --python 3.8 install --dev
You will need to copy `.env.example` to `.env`, then set variables there.
@@ -41,22 +36,29 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
## Running the server
Running the server
------------------
LNbits uses [FastAPI][fastapi] as an application server.
Before running the server for the first time, make sure to create the data folder and edit the .env file:
LNbits uses [Quart][quart] as an application server.
mkdir data
cp .env.example .env
sudo nano .env
```sh
$ pipenv run python -m lnbits
```
To then run the server, use:
Frontend
--------
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/
+122
View File
@@ -0,0 +1,122 @@
## Defining a route with path parameters
**old:**
```python
# with <>
@offlineshop_ext.route("/lnurl/<item_id>", methods=["GET"])
```
**new:**
```python
# with curly braces: {}
@offlineshop_ext.get("/lnurl/{item_id}")
```
## Check if a user exists and access user object
**old:**
```python
# decorators
@check_user_exists()
async def do_routing_stuff():
pass
```
**new:**
If user doesn't exist, `Depends(check_user_exists)` will raise an exception.
If user exists, `user` will be the user object
```python
# depends calls
@core_html_routes.get("/my_route")
async def extensions(user: User = Depends(check_user_exists)):
pass
```
## Returning data from API calls
**old:**
```python
return (
{
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
},
HTTPStatus.OK,
)
```
FastAPI returns `HTTPStatus.OK` by default id no Exception is raised
**new:**
```python
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat
}
```
To change the default HTTPStatus, add it to the path decorator
```python
@core_app.post("/api/v1/payments", status_code=HTTPStatus.CREATED)
async def payments():
pass
```
## Raise exceptions
**old:**
```python
return (
{"message": f"Failed to connect to {domain}."},
HTTPStatus.BAD_REQUEST,
)
# or the Quart way via abort function
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
```
**new:**
Raise an exception to return a status code other than the default status code.
```python
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}."
)
```
## Extensions
**old:**
```python
from quart import Blueprint
amilk_ext: Blueprint = Blueprint(
"amilk", __name__, static_folder="static", template_folder="templates"
)
```
**new:**
```python
from fastapi import APIRouter
from lnbits.jinja2_templating import Jinja2Templates
from lnbits.helpers import template_renderer
from fastapi.staticfiles import StaticFiles
offlineshop_ext: APIRouter = APIRouter(
prefix="/Extension",
tags=["Offlineshop"]
)
offlineshop_ext.mount(
"lnbits/extensions/offlineshop/static",
StaticFiles("lnbits/extensions/offlineshop/static")
)
offlineshop_rndr = template_renderer([
"lnbits/extensions/offlineshop/templates",
])
```
## Possible optimizations
### Use Redis as a cache server
Instead of hitting the database over and over again, we can store a short lived object in [Redis](https://redis.io) for an arbitrary key.
Example:
* Get transactions for a wallet ID
* User data for a user id
* Wallet data for a Admin / Invoice key
+89 -8
View File
@@ -4,27 +4,108 @@ title: Basic installation
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/
Basic installation
==================
# Postgres doesn't have a default password, so we'll create one.
sudo -i -u postgres
psql
# on psql
ALTER USER postgres PASSWORD 'myPassword'; # choose whatever password you want
\q
# on postgres user
createdb lnbits
exit
```
Download this repo and install the dependencies:
```sh
git clone https://github.com/lnbits/lnbits.git
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
# for now you'll need to `git checkout FastAPI`
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env
./venv/bin/quart assets
./venv/bin/quart migrate
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
./venv/bin/uvicorn lnbits.__main__:app --port 5000
```
No you can visit your LNbits at http://localhost:5000/.
Now you can visit your LNbits at http://localhost:5000/.
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
Then you can run restart it and it will be using the new settings.
Then you can restart it and it will be using the new settings.
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
## Important note
If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres!
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above.
```sh
# STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials
python3 conv.py
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
# Additional guides
### LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
### Docker installation
To install using docker you first need to build the docker image as:
```
git clone https://github.com/lnbits/lnbits.git
cd lnbits/ # ${PWD} referred as <lnbits_repo>
docker build -t lnbits .
```
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
```
cp <lnbits_repo>/.env.example .env
```
and change the configuration in `.env` as required.
Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container.
```
mkdir data
sudo chown 1000:1000 ./data/
```
Then the image can be run as:
```
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits
```
Finally you can access your lnbits on your machine at port 5000.
# Additional guides
## LNbits running on Umbrel behind Tor
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
+3 -3
View File
@@ -29,7 +29,7 @@ Using this wallet requires the installation of the `pylightning` Python package.
### LND (gRPC)
Using this wallet requires the installation of the `lndgrpc` and `purerpc` Python packages.
Using this wallet requires the installation of the `grpcio` and `protobuf` Python packages.
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address
@@ -54,7 +54,7 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho
### LNPay
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret.
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
@@ -65,7 +65,7 @@ For the invoice listener to work you have a publicly accessible URL in your LNbi
### lntxbot
- `LNBITS_BACKEND_WALLET_CLASS`: **LntxbotWallet**
- `LNTXBOT_API_ENDPOINT`: https://lntxbot.bigsun.xyz/
- `LNTXBOT_API_ENDPOINT`: https://lntxbot.com/
- `LNTXBOT_KEY`: lntxbotAdminApiKey
+17 -14
View File
@@ -1,8 +1,22 @@
import trio # type: ignore
import asyncio
from .commands import migrate_databases, transpile_scss, bundle_vendored
import uvloop
from starlette.requests import Request
trio.run(migrate_databases)
from .commands import bundle_vendored, migrate_databases, transpile_scss
from .settings import (
DEBUG,
LNBITS_COMMIT,
LNBITS_DATA_FOLDER,
LNBITS_SITE_TITLE,
PORT,
SERVICE_FEE,
WALLET,
)
uvloop.install()
asyncio.create_task(migrate_databases())
transpile_scss()
bundle_vendored()
@@ -10,15 +24,6 @@ from .app import create_app
app = create_app()
from .settings import (
LNBITS_SITE_TITLE,
SERVICE_FEE,
DEBUG,
LNBITS_DATA_FOLDER,
WALLET,
LNBITS_COMMIT,
)
print(
f"""Starting LNbits with
- git version: {LNBITS_COMMIT}
@@ -29,5 +34,3 @@ print(
- service fee: {SERVICE_FEE}
"""
)
app.run(host=app.config["HOST"], port=app.config["PORT"])
+110 -72
View File
@@ -1,60 +1,92 @@
import sys
import asyncio
import importlib
from lnbits.core.tasks import register_task_listeners
import sys
import traceback
import warnings
from quart import g
from quart_trio import QuartTrio
from quart_cors import cors # type: ignore
from quart_compress import Compress # type: ignore
from secure import SecureHeaders # type: ignore
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
import lnbits.settings
from .commands import db_migrate, handle_assets
from .core import core_app
from .core.views.generic import core_html_routes
from .helpers import (
get_valid_extensions,
get_js_vendored,
get_css_vendored,
get_js_vendored,
get_valid_extensions,
template_renderer,
url_for_vendored,
)
from .proxy_fix import ASGIProxyFix
from .tasks import (
run_deferred_async,
check_pending_payments,
invoice_listener,
internal_invoice_listener,
webhook_handler,
grab_app_for_later,
)
from .requestvars import g
from .settings import WALLET
secure_headers = SecureHeaders(hsts=False, xfo=False)
from .tasks import (
catch_everything_and_restart,
check_pending_payments,
internal_invoice_listener,
invoice_listener,
run_deferred_async,
webhook_handler,
)
def create_app(config_object="lnbits.settings") -> QuartTrio:
def create_app(config_object="lnbits.settings") -> FastAPI:
"""Create application factory.
:param config_object: The configuration object to use.
"""
app = QuartTrio(__name__, static_folder="static")
app.config.from_object(config_object)
app.asgi_http_class = ASGIProxyFix
app = FastAPI()
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static")
app.mount(
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static"
)
cors(app)
Compress(app)
origins = ["http://localhost", "http://localhost:5000"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
g().config = lnbits.settings
g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
return template_renderer().TemplateResponse(
"error.html",
{"request": request, "err": f"`{exc.errors()}` is not a valid UUID."},
)
# return HTMLResponse(
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
# )
app.add_middleware(GZipMiddleware, minimum_size=1000)
# app.add_middleware(ASGIProxyFix)
check_funding_source(app)
register_assets(app)
register_blueprints(app)
register_filters(app)
register_commands(app)
register_request_hooks(app)
register_routes(app)
# register_commands(app)
register_async_tasks(app)
grab_app_for_later(app)
register_exception_handlers(app)
return app
def check_funding_source(app: QuartTrio) -> None:
@app.before_serving
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:
@@ -70,72 +102,78 @@ def check_funding_source(app: QuartTrio) -> None:
)
def register_blueprints(app: QuartTrio) -> None:
"""Register Flask blueprints / LNbits extensions."""
app.register_blueprint(core_app)
def register_routes(app: FastAPI) -> None:
"""Register FastAPI routes / LNbits extensions."""
app.include_router(core_app)
app.include_router(core_html_routes)
for ext in get_valid_extensions():
try:
ext_module = importlib.import_module(f"lnbits.extensions.{ext.code}")
bp = getattr(ext_module, f"{ext.code}_ext")
ext_route = getattr(ext_module, f"{ext.code}_ext")
app.register_blueprint(bp, url_prefix=f"/{ext.code}")
except Exception:
if hasattr(ext_module, f"{ext.code}_start"):
ext_start_func = getattr(ext_module, f"{ext.code}_start")
ext_start_func()
if hasattr(ext_module, f"{ext.code}_static_files"):
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
for s in ext_statics:
app.mount(s["path"], s["app"], s["name"])
app.include_router(ext_route)
except Exception as e:
print(str(e))
raise ImportError(
f"Please make sure that the extension `{ext.code}` follows conventions."
)
def register_commands(app: QuartTrio):
def register_commands(app: FastAPI):
"""Register Click commands."""
app.cli.add_command(db_migrate)
app.cli.add_command(handle_assets)
def register_assets(app: QuartTrio):
def register_assets(app: FastAPI):
"""Serve each vendored asset separately or a bundle."""
@app.before_request
@app.on_event("startup")
async def vendored_assets_variable():
if app.config["DEBUG"]:
g.VENDORED_JS = map(url_for_vendored, get_js_vendored())
g.VENDORED_CSS = map(url_for_vendored, get_css_vendored())
if g().config.DEBUG:
g().VENDORED_JS = map(url_for_vendored, get_js_vendored())
g().VENDORED_CSS = map(url_for_vendored, get_css_vendored())
else:
g.VENDORED_JS = ["/static/bundle.js"]
g.VENDORED_CSS = ["/static/bundle.css"]
def register_filters(app: QuartTrio):
"""Jinja filters."""
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
def register_request_hooks(app: QuartTrio):
"""Open the core db for each request so everything happens in a big transaction"""
@app.before_request
async def before_request():
g.nursery = app.nursery
@app.after_request
async def set_secure_headers(response):
secure_headers.quart(response)
return response
g().VENDORED_JS = ["/static/bundle.js"]
g().VENDORED_CSS = ["/static/bundle.css"]
def register_async_tasks(app):
@app.route("/wallet/webhook", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@app.route("/wallet/webhook")
async def webhook_listener():
return await webhook_handler()
@app.before_serving
@app.on_event("startup")
async def listeners():
run_deferred_async(app.nursery)
app.nursery.start_soon(check_pending_payments)
app.nursery.start_soon(invoice_listener, app.nursery)
app.nursery.start_soon(internal_invoice_listener, app.nursery)
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(check_pending_payments))
loop.create_task(catch_everything_and_restart(invoice_listener))
loop.create_task(catch_everything_and_restart(internal_invoice_listener))
await register_task_listeners()
await run_deferred_async()
@app.after_serving
@app.on_event("shutdown")
async def stop_listeners():
pass
def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception)
async def basic_error(request: Request, err):
print("handled error", traceback.format_exc())
etype, _, tb = sys.exc_info()
traceback.print_exception(etype, err, tb)
exc = traceback.format_exc()
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": err}
)
+1 -6
View File
@@ -125,12 +125,7 @@ def _unshorten_amount(amount: str) -> int:
# * `u` (micro): multiply by 0.000001
# * `n` (nano): multiply by 0.000000001
# * `p` (pico): multiply by 0.000000000001
units = {
"p": 10 ** 12,
"n": 10 ** 9,
"u": 10 ** 6,
"m": 10 ** 3,
}
units = {"p": 10 ** 12, "n": 10 ** 9, "u": 10 ** 6, "m": 10 ** 3}
unit = str(amount)[-1]
# BOLT #11:
+56 -36
View File
@@ -1,11 +1,11 @@
import trio # type: ignore
import asyncio
import warnings
import click
import importlib
import re
import os
from sqlalchemy.exc import OperationalError # type: ignore
from .db import SQLITE, POSTGRES, COCKROACH
from .core import db as core_db, migrations as core_migrations
from .helpers import (
get_valid_extensions,
@@ -18,7 +18,7 @@ from .settings import LNBITS_PATH
@click.command("migrate")
def db_migrate():
trio.run(migrate_databases)
asyncio.create_task(migrate_databases())
@click.command("assets")
@@ -53,41 +53,61 @@ def bundle_vendored():
async def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them."""
async with core_db.connect() as conn:
try:
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
except OperationalError:
# migration 3 wasn't ran
await core_migrations.m000_create_migrations_table(conn)
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
async def set_migration_version(conn, db_name, version):
await conn.execute(
"""
INSERT INTO dbversions (db, version) VALUES (?, ?)
ON CONFLICT (db) DO UPDATE SET version = ?
""",
(db_name, version, version),
)
async def run_migration(db, migrations_module):
db_name = migrations_module.__name__.split(".")[-2]
for key, migrate in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
await migrate(db)
if db.schema == None:
await set_migration_version(db, db_name, version)
else:
async with core_db.connect() as conn:
await set_migration_version(conn, db_name, version)
async with core_db.connect() as conn:
if conn.type == SQLITE:
exists = await conn.fetchone(
"SELECT * FROM sqlite_master WHERE type='table' AND name='dbversions'"
)
elif conn.type in {POSTGRES, COCKROACH}:
exists = await conn.fetchone(
"SELECT * FROM information_schema.tables WHERE table_name = 'dbversions'"
)
if not exists:
await core_migrations.m000_create_migrations_table(conn)
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
current_versions = {row["db"]: row["version"] for row in rows}
matcher = re.compile(r"^m(\d\d\d)_")
async def run_migration(db, migrations_module):
db_name = migrations_module.__name__.split(".")[-2]
for key, migrate in migrations_module.__dict__.items():
match = match = matcher.match(key)
if match:
version = int(match.group(1))
if version > current_versions.get(db_name, 0):
print(f"running migration {db_name}.{version}")
await migrate(db)
await conn.execute(
"INSERT OR REPLACE INTO dbversions (db, version) VALUES (?, ?)",
(db_name, version),
)
await run_migration(conn, core_migrations)
for ext in get_valid_extensions():
try:
ext_migrations = importlib.import_module(
f"lnbits.extensions.{ext.code}.migrations"
)
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
await run_migration(ext_db, ext_migrations)
except ImportError:
raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
for ext in get_valid_extensions():
try:
ext_migrations = importlib.import_module(
f"lnbits.extensions.{ext.code}.migrations"
)
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
except ImportError:
raise ImportError(
f"Please make sure that the extension `{ext.code}` has a migrations file."
)
async with ext_db.connect() as ext_conn:
await run_migration(ext_conn, ext_migrations)
print(" ✔️ All migrations done.")
+4 -14
View File
@@ -1,21 +1,11 @@
from quart import Blueprint
from fastapi.routing import APIRouter
from lnbits.db import Database
db = Database("database")
core_app: Blueprint = Blueprint(
"core",
__name__,
template_folder="templates",
static_folder="static",
static_url_path="/core/static",
)
core_app: APIRouter = APIRouter()
from .views.api import * # noqa
from .views.generic import * # noqa
from .tasks import register_listeners
from lnbits.tasks import record_async
core_app.record(record_async(register_listeners))
from .views.public_api import * # noqa
+72 -64
View File
@@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any
from urllib.parse import urlparse
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME
from . import db
@@ -43,41 +43,37 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
if user:
extensions = await (conn or db).fetchall(
"SELECT extension FROM extensions WHERE user = ? AND active = 1", (user_id,)
"""SELECT extension FROM extensions WHERE "user" = ? AND active""",
(user_id,),
)
wallets = await (conn or db).fetchall(
"""
SELECT *, COALESCE((SELECT balance FROM balances WHERE wallet = wallets.id), 0) AS balance_msat
FROM wallets
WHERE user = ?
WHERE "user" = ?
""",
(user_id,),
)
else:
return None
return (
User(
**{
**user,
**{
"extensions": [e[0] for e in extensions],
"wallets": [Wallet(**w) for w in wallets],
},
}
)
if user
else None
return User(
id=user["id"],
email=user["email"],
extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets],
)
async def update_user_extension(
*, user_id: str, extension: str, active: int, conn: Optional[Connection] = None
*, user_id: str, extension: str, active: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"""
INSERT OR REPLACE INTO extensions (user, extension, active)
VALUES (?, ?, ?)
INSERT INTO extensions ("user", extension, active) VALUES (?, ?, ?)
ON CONFLICT ("user", extension) DO UPDATE SET active = ?
""",
(user_id, extension, active),
(user_id, extension, active, active),
)
@@ -94,7 +90,7 @@ async def create_wallet(
wallet_id = uuid4().hex
await (conn or db).execute(
"""
INSERT INTO wallets (id, name, user, adminkey, inkey)
INSERT INTO wallets (id, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
""",
(
@@ -112,6 +108,19 @@ async def create_wallet(
return new_wallet
async def update_wallet(
wallet_id: str, new_name: str, conn: Optional[Connection] = None
) -> Optional[Wallet]:
await (conn or db).execute(
"""
UPDATE wallets SET
name = ?
WHERE id = ?
""",
(new_name, wallet_id),
)
async def delete_wallet(
*, user_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
@@ -119,10 +128,10 @@ async def delete_wallet(
"""
UPDATE wallets AS w
SET
user = 'del:' || w.user,
"user" = 'del:' || w."user",
adminkey = 'del:' || w.adminkey,
inkey = 'del:' || w.inkey
WHERE id = ? AND user = ?
WHERE id = ? AND "user" = ?
""",
(wallet_id, user_id),
)
@@ -218,7 +227,12 @@ async def get_payments(
clause: List[str] = []
if since != None:
clause.append("time > ?")
if db.type == POSTGRES:
clause.append("time > to_timestamp(?)")
elif db.type == COCKROACH:
clause.append("time > cast(? AS timestamp)")
else:
clause.append("time > ?")
args.append(since)
if wallet_id:
@@ -228,9 +242,9 @@ async def get_payments(
if complete and pending:
pass
elif complete:
clause.append("((amount > 0 AND pending = 0) OR amount < 0)")
clause.append("((amount > 0 AND pending = false) OR amount < 0)")
elif pending:
clause.append("pending = 1")
clause.append("pending = true")
else:
pass
@@ -264,15 +278,25 @@ async def get_payments(
return [Payment.from_row(row) for row in rows]
async def delete_expired_invoices(
conn: Optional[Connection] = None,
) -> None:
rows = await (conn or db).fetchall(
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"""
DELETE FROM apipayments
WHERE pending = true AND amount > 0
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
"""
)
# then we delete all expired invoices, checking one by one
rows = await (conn or db).fetchall(
f"""
SELECT bolt11
FROM apipayments
WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400
"""
WHERE pending = true
AND bolt11 IS NOT NULL
AND amount > 0 AND time < {db.timestamp_now} - {db.interval_seconds(86400)}
"""
)
for (payment_request,) in rows:
try:
@@ -287,7 +311,7 @@ async def delete_expired_invoices(
await (conn or db).execute(
"""
DELETE FROM apipayments
WHERE pending = 1 AND hash = ?
WHERE pending = true AND hash = ?
""",
(invoice.payment_hash,),
)
@@ -326,7 +350,7 @@ async def create_payment(
payment_hash,
preimage,
amount,
int(pending),
pending,
memo,
fee,
json.dumps(extra)
@@ -343,36 +367,27 @@ async def create_payment(
async def update_payment_status(
checking_id: str,
pending: bool,
conn: Optional[Connection] = None,
checking_id: str, pending: bool, conn: Optional[Connection] = None
) -> None:
await (conn or db).execute(
"UPDATE apipayments SET pending = ? WHERE checking_id = ?",
(
int(pending),
checking_id,
),
(pending, checking_id),
)
async def delete_payment(
checking_id: str,
conn: Optional[Connection] = None,
) -> None:
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
)
async def check_internal(
payment_hash: str,
conn: Optional[Connection] = None,
payment_hash: str, conn: Optional[Connection] = None
) -> Optional[str]:
row = await (conn or db).fetchone(
"""
SELECT checking_id FROM apipayments
WHERE hash = ? AND pending AND amount > 0
WHERE hash = ? AND pending AND amount > 0
""",
(payment_hash,),
)
@@ -387,25 +402,21 @@ async def check_internal(
async def save_balance_check(
wallet_id: str,
url: str,
conn: Optional[Connection] = None,
wallet_id: str, url: str, conn: Optional[Connection] = None
):
domain = urlparse(url).netloc
await (conn or db).execute(
"""
INSERT OR REPLACE INTO balance_check (wallet, service, url)
VALUES (?, ?, ?)
INSERT INTO balance_check (wallet, service, url) VALUES (?, ?, ?)
ON CONFLICT (wallet, service) DO UPDATE SET url = ?
""",
(wallet_id, domain, url),
(wallet_id, domain, url, url),
)
async def get_balance_check(
wallet_id: str,
domain: str,
conn: Optional[Connection] = None,
wallet_id: str, domain: str, conn: Optional[Connection] = None
) -> Optional[BalanceCheck]:
row = await (conn or db).fetchone(
"""
@@ -428,22 +439,19 @@ async def get_balance_checks(conn: Optional[Connection] = None) -> List[BalanceC
async def save_balance_notify(
wallet_id: str,
url: str,
conn: Optional[Connection] = None,
wallet_id: str, url: str, conn: Optional[Connection] = None
):
await (conn or db).execute(
"""
INSERT OR REPLACE INTO balance_notify (wallet, url)
VALUES (?, ?)
INSERT INTO balance_notify (wallet, url) VALUES (?, ?)
ON CONFLICT (wallet) DO UPDATE SET url = ?
""",
(wallet_id, url),
(wallet_id, url, url),
)
async def get_balance_notify(
wallet_id: str,
conn: Optional[Connection] = None,
wallet_id: str, conn: Optional[Connection] = None
) -> Optional[str]:
row = await (conn or db).fetchone(
"""
+17 -19
View File
@@ -4,7 +4,7 @@ from sqlalchemy.exc import OperationalError # type: ignore
async def m000_create_migrations_table(db):
await db.execute(
"""
CREATE TABLE dbversions (
CREATE TABLE IF NOT EXISTS dbversions (
db TEXT PRIMARY KEY,
version INT NOT NULL
)
@@ -28,11 +28,11 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS extensions (
user TEXT NOT NULL,
"user" TEXT NOT NULL,
extension TEXT NOT NULL,
active BOOLEAN DEFAULT 0,
active BOOLEAN DEFAULT false,
UNIQUE (user, extension)
UNIQUE ("user", extension)
);
"""
)
@@ -41,14 +41,14 @@ async def m001_initial(db):
CREATE TABLE IF NOT EXISTS wallets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
user TEXT NOT NULL,
"user" TEXT NOT NULL,
adminkey TEXT NOT NULL,
inkey TEXT
);
"""
)
await db.execute(
"""
f"""
CREATE TABLE IF NOT EXISTS apipayments (
payhash TEXT NOT NULL,
amount INTEGER NOT NULL,
@@ -56,8 +56,7 @@ async def m001_initial(db):
wallet TEXT NOT NULL,
pending BOOLEAN NOT NULL,
memo TEXT,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
UNIQUE (wallet, payhash)
);
"""
@@ -65,18 +64,18 @@ async def m001_initial(db):
await db.execute(
"""
CREATE VIEW IF NOT EXISTS balances AS
CREATE VIEW balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
FROM apipayments
WHERE amount > 0 AND pending = 0 -- don't sum pending
WHERE amount > 0 AND pending = false -- don't sum pending
GROUP BY wallet
UNION ALL
SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees
FROM apipayments
WHERE amount < 0 -- do sum pending
GROUP BY wallet
)
)x
GROUP BY wallet;
"""
)
@@ -143,21 +142,20 @@ async def m004_ensure_fees_are_always_negative(db):
"""
await db.execute("DROP VIEW balances")
await db.execute(
"""
CREATE VIEW IF NOT EXISTS balances AS
CREATE VIEW balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
FROM apipayments
WHERE amount > 0 AND pending = 0 -- don't sum pending
WHERE amount > 0 AND pending = false -- don't sum pending
GROUP BY wallet
UNION ALL
SELECT wallet, SUM(amount - abs(fee)) AS s -- outgoing, sum fees
FROM apipayments
WHERE amount < 0 -- do sum pending
GROUP BY wallet
)
)x
GROUP BY wallet;
"""
)
@@ -170,8 +168,8 @@ async def m005_balance_check_balance_notify(db):
await db.execute(
"""
CREATE TABLE balance_check (
wallet INTEGER NOT NULL REFERENCES wallets (id),
CREATE TABLE IF NOT EXISTS balance_check (
wallet TEXT NOT NULL REFERENCES wallets (id),
service TEXT NOT NULL,
url TEXT NOT NULL,
@@ -182,8 +180,8 @@ async def m005_balance_check_balance_notify(db):
await db.execute(
"""
CREATE TABLE balance_notify (
wallet INTEGER NOT NULL REFERENCES wallets (id),
CREATE TABLE IF NOT EXISTS balance_notify (
wallet TEXT NOT NULL REFERENCES wallets (id),
url TEXT NOT NULL,
UNIQUE(wallet, url)
+30 -56
View File
@@ -1,32 +1,16 @@
import json
import hmac
import hashlib
from quart import url_for
from lnbits.helpers import url_for
from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
from pydantic import BaseModel
from lnbits.settings import WALLET
class User(NamedTuple):
id: str
email: str
extensions: List[str] = []
wallets: List["Wallet"] = []
password: Optional[str] = None
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
class Wallet(NamedTuple):
class Wallet(BaseModel):
id: str
name: str
user: str
@@ -46,12 +30,8 @@ class Wallet(NamedTuple):
@property
def lnurlwithdraw_full(self) -> str:
url = url_for(
"core.lnurl_full_withdraw",
usr=self.user,
wal=self.id,
_external=True,
)
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
try:
return lnurl_encode(url)
except:
@@ -62,51 +42,45 @@ class Wallet(NamedTuple):
linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256")
return SigningKey.from_string(
linking_key,
curve=SECP256k1,
hashfunc=hashlib.sha256,
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
)
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
from .crud import get_wallet_payment
from .crud import get_standalone_payment
return await get_wallet_payment(self.id, payment_hash)
async def get_payments(
self,
*,
complete: bool = True,
pending: bool = False,
outgoing: bool = True,
incoming: bool = True,
exclude_uncheckable: bool = False,
) -> List["Payment"]:
from .crud import get_payments
return await get_payments(
wallet_id=self.id,
complete=complete,
pending=pending,
outgoing=outgoing,
incoming=incoming,
exclude_uncheckable=exclude_uncheckable,
)
return await get_standalone_payment(payment_hash)
class Payment(NamedTuple):
class User(BaseModel):
id: str
email: Optional[str] = None
extensions: List[str] = []
wallets: List[Wallet] = []
password: Optional[str] = None
@property
def wallet_ids(self) -> List[str]:
return [wallet.id for wallet in self.wallets]
def get_wallet(self, wallet_id: str) -> Optional["Wallet"]:
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
return w[0] if w else None
class Payment(BaseModel):
checking_id: str
pending: bool
amount: int
fee: int
memo: str
memo: Optional[str]
time: int
bolt11: str
preimage: str
payment_hash: str
extra: Dict
extra: Optional[Dict] = {}
wallet_id: str
webhook: str
webhook_status: int
webhook: Optional[str]
webhook_status: Optional[int]
@classmethod
def from_row(cls, row: Row):
@@ -181,7 +155,7 @@ class Payment(NamedTuple):
await delete_payment(self.checking_id)
class BalanceCheck(NamedTuple):
class BalanceCheck(BaseModel):
wallet: str
service: str
url: str
+78 -66
View File
@@ -1,34 +1,36 @@
import trio # type: ignore
import asyncio
import json
import httpx
from io import BytesIO
from binascii import unhexlify
from typing import Optional, Tuple, Dict
from urllib.parse import urlparse, parse_qs
from quart import g, url_for
from lnurl import LnurlErrorResponse, decode as decode_lnurl # type: ignore
from io import BytesIO
from typing import Dict, Optional, Tuple
from urllib.parse import parse_qs, urlparse
import httpx
from lnurl import LnurlErrorResponse
from lnurl import decode as decode_lnurl # type: ignore
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentResponse, PaymentStatus
from . import db
from .crud import (
check_internal,
create_payment,
delete_payment,
get_wallet,
get_wallet_payment,
update_payment_status,
)
try:
from typing import TypedDict # type: ignore
except ImportError: # pragma: nocover
from typing_extensions import TypedDict
from lnbits import bolt11
from lnbits.db import Connection
from lnbits.helpers import urlsafe_short_hash
from lnbits.settings import WALLET
from lnbits.wallets.base import PaymentStatus, PaymentResponse
from . import db
from .crud import (
get_wallet,
create_payment,
delete_payment,
check_internal,
update_payment_status,
get_wallet_payment,
)
class PaymentFailure(Exception):
pass
@@ -49,7 +51,6 @@ async def create_invoice(
conn: Optional[Connection] = None,
) -> Tuple[str, str]:
invoice_memo = None if description_hash else memo
storeable_memo = memo
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
amount=amount, memo=invoice_memo, description_hash=description_hash
@@ -66,7 +67,7 @@ async def create_invoice(
payment_request=payment_request,
payment_hash=invoice.payment_hash,
amount=amount_msat,
memo=storeable_memo,
memo=memo,
extra=extra,
webhook=webhook,
conn=conn,
@@ -94,6 +95,10 @@ async def pay_invoice(
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",
@@ -142,24 +147,25 @@ async def pay_invoice(
if wallet.balance_msat < 0:
raise PermissionError("Insufficient balance.")
if internal_checking_id:
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
if internal_checking_id:
# mark the invoice from the other side as not pending anymore
# so the other side only has access to his new money when we are sure
# the payer has enough to deduct from
async with db.connect() as conn:
await update_payment_status(
checking_id=internal_checking_id,
pending=False,
conn=conn,
checking_id=internal_checking_id, pending=False, conn=conn
)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_paid
# notify receiver asynchronously
await internal_invoice_paid.send(internal_checking_id)
else:
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
if payment.checking_id:
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(internal_checking_id)
else:
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
if payment.checking_id:
async with db.connect() as conn:
await create_payment(
checking_id=payment.checking_id,
fee=payment.fee_msat,
@@ -169,13 +175,15 @@ async def pay_invoice(
**payment_kwargs,
)
await delete_payment(temp_id, conn=conn)
else:
raise PaymentFailure(
payment.error_message
or "Payment failed, but backend didn't give us an error message."
)
else:
async with db.connect() as conn:
await delete_payment(temp_id, conn=conn)
raise PaymentFailure(
payment.error_message
or "Payment failed, but backend didn't give us an error message."
)
return invoice.payment_hash
return invoice.payment_hash
async def redeem_lnurl_withdraw(
@@ -211,38 +219,33 @@ async def redeem_lnurl_withdraw(
return None
if wait_seconds:
await trio.sleep(wait_seconds)
await asyncio.sleep(wait_seconds)
params = {
"k1": res["k1"],
"pr": payment_request,
}
params = {"k1": res["k1"], "pr": payment_request}
try:
params["balanceNotify"] = url_for(
"core.lnurl_balance_notify",
service=urlparse(lnurl_request).netloc,
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
external=True,
wal=wallet_id,
_external=True,
)
except Exception:
pass
async with httpx.AsyncClient() as client:
await client.get(
res["callback"],
params=params,
)
try:
await client.get(res["callback"], params=params)
except Exception:
pass
async def perform_lnurlauth(
callback: str,
conn: Optional[Connection] = None,
callback: str, conn: Optional[Connection] = None
) -> Optional[LnurlErrorResponse]:
cb = urlparse(callback)
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
key = g.wallet.lnurlauth_key(cb.netloc)
key = g().wallet.lnurlauth_key(cb.netloc)
def int_to_bytes_suitable_der(x: int) -> bytes:
"""for strict DER we need to encode the integer with some quirks"""
@@ -305,20 +308,29 @@ async def perform_lnurlauth(
return LnurlErrorResponse(reason=resp["reason"])
except (KeyError, json.decoder.JSONDecodeError):
return LnurlErrorResponse(
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
)
async def check_invoice_status(
wallet_id: str,
payment_hash: str,
conn: Optional[Connection] = None,
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
) -> PaymentStatus:
payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
if not payment:
return PaymentStatus(None)
return await WALLET.get_invoice_status(payment.checking_id)
status = await WALLET.get_invoice_status(payment.checking_id)
if not payment.pending:
return status
if payment.is_out and status.failed:
print(
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
await payment.delete()
elif not status.pending:
print(
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
)
await payment.set_pending(status.pending)
return status
def fee_reserve(amount_msat: int) -> int:
+45 -5
View File
@@ -119,6 +119,8 @@ new Vue({
paymentHash: null,
minMax: [0, 2100000000000000],
lnurl: null,
units: ['sat'],
unit: 'sat',
data: {
amount: null,
memo: ''
@@ -182,7 +184,8 @@ new Vue({
show: false,
location: window.location
},
balance: 0
balance: 0,
newName: ''
}
},
computed: {
@@ -200,9 +203,7 @@ new Vue({
return this.parse.invoice.sat <= this.balance
},
pendingPaymentsExist: function () {
return this.payments
? _.where(this.payments, {pending: 1}).length > 0
: false
return this.payments.findIndex(payment => payment.pending) !== -1
}
},
filters: {
@@ -233,6 +234,7 @@ new Vue({
this.receive.paymentHash = null
this.receive.data.amount = null
this.receive.data.memo = null
this.receive.unit = 'sat'
this.receive.paymentChecker = null
this.receive.minMax = [0, 2100000000000000]
this.receive.lnurl = null
@@ -269,11 +271,13 @@ new Vue({
},
createInvoice: function () {
this.receive.status = 'loading'
LNbits.api
.createInvoice(
this.g.wallet,
this.receive.data.amount,
this.receive.data.memo,
this.receive.unit,
this.receive.lnurl && this.receive.lnurl.callback
)
.then(response => {
@@ -342,7 +346,10 @@ new Vue({
.split('&')[0]
}
if (this.parse.data.request.toLowerCase().startsWith('lnurl1')) {
if (
this.parse.data.request.toLowerCase().startsWith('lnurl1') ||
this.parse.data.request.match(/[\w.+-~_]+@[\w.+-~_]/)
) {
LNbits.api
.request(
'GET',
@@ -580,6 +587,30 @@ new Vue({
}
})
},
updateWalletName: function () {
let newName = this.newName
if (!newName || !newName.length) return
// let data = {name: newName}
LNbits.api
.request('PUT', '/api/v1/wallet/' + newName, this.g.wallet.inkey, {})
.then(res => {
this.newName = ''
this.$q.notify({
message: `Wallet named updated.`,
type: 'positive',
timeout: 3500
})
LNbits.href.updateWallet(
res.data.name,
this.user.id,
this.g.wallet.id
)
})
.catch(err => {
this.newName = ''
LNbits.utils.notifyApiError(err)
})
},
deleteWallet: function (walletId, user) {
LNbits.utils
.confirmDialog('Are you sure you want to delete this wallet?')
@@ -619,6 +650,15 @@ new Vue({
created: function () {
this.fetchBalance()
this.fetchPayments()
LNbits.api
.request('GET', '/api/v1/currencies')
.then(response => {
this.receive.units = ['sat', ...response.data]
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
mounted: function () {
// show disclaimer
+18 -23
View File
@@ -1,4 +1,4 @@
import trio # type: ignore
import asyncio
import httpx
from typing import List
@@ -8,19 +8,21 @@ from . import db
from .crud import get_balance_notify
from .models import Payment
sse_listeners: List[trio.MemorySendChannel] = []
api_invoice_listeners: List[asyncio.Queue] = []
async def register_listeners():
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(5)
register_invoice_listener(invoice_paid_chan_send)
await wait_for_paid_invoices(invoice_paid_chan_recv)
async def register_task_listeners():
invoice_paid_queue = asyncio.Queue(5)
register_invoice_listener(invoice_paid_queue)
asyncio.create_task(wait_for_paid_invoices(invoice_paid_queue))
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
async for payment in invoice_paid_chan:
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
while True:
payment = await invoice_paid_queue.get()
# send information to sse channel
await dispatch_sse(payment)
await dispatch_invoice_listener(payment)
# dispatch webhook
if payment.webhook and not payment.webhook_status:
@@ -31,33 +33,26 @@ async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
if url:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
url,
timeout=4,
)
r = await client.post(url, timeout=4)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
pass
async def dispatch_sse(payment: Payment):
for send_channel in sse_listeners:
async def dispatch_invoice_listener(payment: Payment):
for send_channel in api_invoice_listeners:
try:
send_channel.send_nowait(payment)
except trio.WouldBlock:
send_channel.put_nowait(payment)
except asyncio.QueueFull:
print("removing sse listener", send_channel)
sse_listeners.remove(send_channel)
api_invoice_listeners.remove(send_channel)
async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient() as client:
data = payment._asdict()
try:
r = await client.post(
payment.webhook,
json=data,
timeout=40,
)
r = await client.post(payment.webhook, json=data, timeout=40)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
+34 -5
View File
@@ -19,7 +19,7 @@
<q-card-section>
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i>{{ wallet.adminkey }}</i>"}</code><br />
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
@@ -29,7 +29,7 @@
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl {{ request.url_root }}api/v1/wallet -H "X-Api-Key:
>curl {{ request.base_url }}api/v1/wallet -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>"</code
>
</q-card-section>
@@ -59,7 +59,7 @@
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false,
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code
@@ -86,7 +86,7 @@
<code>{"payment_hash": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": true,
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": true,
"bolt11": &lt;string&gt;}' -H "X-Api-Key:
<i>{{ wallet.adminkey }}"</i> -H "Content-type:
application/json"</code
@@ -94,6 +94,35 @@
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Decode an invoice"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/api/v1/payments/decode</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"invoice": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/payments/decode -d
'{"data": &lt;bolt11/lnurl, string&gt;}' -H "X-Api-Key:
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
@@ -115,7 +144,7 @@
<code>{"paid": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
>curl -X GET {{ request.base_url
}}api/v1/payments/&lt;payment_hash&gt; -H "X-Api-Key:
<i>{{ wallet.inkey }}"</i> -H "Content-type: application/json"</code
>
+3 -3
View File
@@ -17,14 +17,14 @@
></q-icon>
{% raw %}
<h5 class="q-mt-lg q-mb-xs">{{ extension.name }}</h5>
{{ extension.shortDescription }} {% endraw %}
<small>{{ extension.shortDescription }} </small>{% endraw %}
</q-card-section>
<q-separator></q-separator>
<q-card-actions>
<div v-if="extension.isEnabled">
<q-btn
flat
color="deep-purple"
color="primary"
type="a"
:href="[extension.url, '?usr=', g.user.id].join('')"
>Open</q-btn
@@ -41,7 +41,7 @@
<q-btn
v-else
flat
color="deep-purple"
color="primary"
type="a"
:href="['{{ url_for('core.extensions') }}', '?usr=', g.user.id, '&enable=', extension.code].join('')"
>
+55 -47
View File
@@ -8,7 +8,7 @@
{% if lnurl %}
<q-btn
unelevated
color="deep-purple"
color="primary"
@click="processing"
type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}"
@@ -21,11 +21,11 @@
filled
dense
v-model="walletName"
label="Name your LNbits wallet *"
label="Name your {{SITE_TITLE}} wallet *"
></q-input>
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="walletName == ''"
type="submit"
>Add a new wallet</q-btn
@@ -37,58 +37,66 @@
<q-card>
<q-card-section>
<h3 class="q-my-none"><strong>LN</strong>bits</h3>
<h5 class="q-my-md">Free and open-source lightning wallet</h5>
<p>
Easy to set up and lightweight, LNbits can run on any
lightning-network funding source, currently supporting LND,
c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian solution
for others.
</p>
<p>
Each wallet has its own API keys and there is no limit to the number
of wallets you can make. Being able to partition funds makes LNbits a
useful tool for money management and as a development tool.
</p>
<p>
Extensions add extra functionality to LNbits so you can experiment
with a range of cutting-edge technologies on the lightning network. We
have made developing extensions as easy as possible, and as a free and
open-source project, we encourage people to develop and submit their
own.
</p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
color="grey"
type="a"
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
<q-btn
outline
color="grey"
type="a"
href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
<h3 class="q-my-none">{{SITE_TITLE}}</h3>
<h5 class="q-my-md">{{SITE_TAGLINE}}</h5>
<div v-if="'{{SITE_TITLE}}' == 'LNbits'">
<p>
Easy to set up and lightweight, LNbits can run on any
lightning-network funding source, currently supporting LND,
c-lightning, OpenNode, lntxbot, LNPay and even LNbits itself!
</p>
<p>
You can run LNbits for yourself, or easily offer a custodian
solution for others.
</p>
<p>
Each wallet has its own API keys and there is no limit to the number
of wallets you can make. Being able to partition funds makes LNbits
a useful tool for money management and as a development tool.
</p>
<p>
Extensions add extra functionality to LNbits so you can experiment
with a range of cutting-edge technologies on the lightning network.
We have made developing extensions as easy as possible, and as a
free and open-source project, we encourage people to develop and
submit their own.
</p>
<div class="row q-mt-md q-gutter-sm">
<q-btn
outline
color="grey"
type="a"
href="https://github.com/lnbits/lnbits"
target="_blank"
rel="noopener"
>View project in GitHub</q-btn
>
<q-btn
outline
color="grey"
type="a"
href="https://lnbits.com/paywall/GAqKguK5S8f6w5VNjS9DfK"
target="_blank"
rel="noopener"
>Donate</q-btn
>
</div>
</div>
<p v-else>{{SITE_DESCRIPTION}}</p>
</q-card-section>
</q-card>
</div>
<!-- Ads -->
<div class="col-12 col-md-3 col-lg-3">
<div class="col-12 col-md-3 col-lg-3" v-if="'{{SITE_TITLE}}' == 'LNbits'">
<div class="row q-col-gutter-lg justify-center">
<div class="col-6 col-sm-4 col-md-8 q-gutter-y-sm">
<q-btn flat color="purple" label="Runs on" class="full-width"></q-btn>
<q-btn
flat
color="secondary"
label="Runs on"
class="full-width"
></q-btn>
<div class="row">
<div class="col">
<a href="https://github.com/ElementsProject/lightning">
+50 -17
View File
@@ -22,7 +22,7 @@
<div class="col">
<q-btn
unelevated
color="deep-purple"
color="primary"
class="full-width"
@click="showParseDialog"
>Paste Request</q-btn
@@ -31,7 +31,7 @@
<div class="col">
<q-btn
unelevated
color="deep-purple"
color="primary"
class="full-width"
@click="showReceiveDialog"
>Create Invoice</q-btn
@@ -40,7 +40,7 @@
<div class="col">
<q-btn
unelevated
color="purple"
color="secondary"
icon="photo_camera"
@click="showCamera"
>scan
@@ -92,6 +92,7 @@
:columns="paymentsTable.columns"
:pagination.sync="paymentsTable.pagination"
no-data-label="No transactions made yet"
:filter="paymentsTable.filter"
>
{% raw %}
<template v-slot:header="props">
@@ -129,7 +130,7 @@
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a
class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')"
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>
@@ -222,7 +223,7 @@
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
LNbits wallet: <strong><em>{{ wallet.name }}</em></strong>
{{ SITE_TITLE }} wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
@@ -269,13 +270,37 @@
can scan it from your phone to open your wallet from there.
</p>
<qrcode
:value="'{{request.url_root}}'+'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</q-card-section>
</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"
@@ -313,12 +338,21 @@
<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 (sat) *"
: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"
@@ -327,13 +361,13 @@
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
label="Memo *"
placeholder="LNbits invoice"
></q-input>
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
@@ -346,7 +380,7 @@
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="deep-purple"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
@@ -386,7 +420,7 @@
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="deep-purple" @click="payInvoice">Pay</q-btn>
<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">
@@ -414,7 +448,7 @@
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit">Login</q-btn>
<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
>
@@ -434,7 +468,8 @@
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.domain }}</b> is requesting <br />
<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">
@@ -476,9 +511,7 @@
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="deep-purple" type="submit"
>Send satoshis</q-btn
>
<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
>
@@ -503,7 +536,7 @@
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
+376 -310
View File
@@ -1,113 +1,144 @@
import trio # type: ignore
import asyncio
import hashlib
import json
import lnurl # type: ignore
import httpx
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, jsonify, make_response, url_for
from http import HTTPStatus
from binascii import unhexlify
from typing import Dict, Union
from http import HTTPStatus
from typing import Dict, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
from lnbits import bolt11
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
import httpx
from fastapi import Query, Request
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse
from lnbits import bolt11, lnurl
from lnbits.bolt11 import Invoice
from lnbits.core.models import Payment, Wallet
from lnbits.decorators import (
WalletAdminKeyChecker,
WalletInvoiceKeyChecker,
WalletTypeInfo,
get_key_type,
)
from lnbits.helpers import url_for
from lnbits.requestvars import g
from lnbits.utils.exchange_rates import currencies, fiat_amount_as_satoshis
from .. import core_app, db
from ..crud import save_balance_check
from ..crud import (
get_payments,
get_standalone_payment,
save_balance_check,
update_wallet,
)
from ..services import (
PaymentFailure,
InvoiceFailure,
PaymentFailure,
check_invoice_status,
create_invoice,
pay_invoice,
perform_lnurlauth,
)
from ..tasks import sse_listeners
from ..tasks import api_invoice_listeners
@core_app.route("/api/v1/wallet", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_wallet():
return (
jsonify(
{
"id": g.wallet.id,
"name": g.wallet.name,
"balance": g.wallet.balance_msat,
}
),
HTTPStatus.OK,
)
@core_app.get("/api/v1/wallet")
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet_type == 0:
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
else:
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
@core_app.route("/api/v1/payments", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_payments():
return jsonify(await g.wallet.get_payments(pending=True)), HTTPStatus.OK
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"amount": {"type": "integer", "min": 1, "required": True},
"memo": {
"type": "string",
"empty": False,
"required": True,
"excludes": "description_hash",
},
"description_hash": {
"type": "string",
"empty": False,
"required": True,
"excludes": "memo",
},
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
"lnurl_balance_check": {"type": "string", "required": False},
"extra": {"type": "dict", "nullable": True, "required": False},
"webhook": {"type": "string", "empty": False, "required": False},
@core_app.put("/api/v1/wallet/{new_name}")
async def api_update_wallet(
new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker())
):
await update_wallet(wallet.wallet.id, new_name)
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
)
async def api_payments_create_invoice():
if "description_hash" in g.data:
description_hash = unhexlify(g.data["description_hash"])
@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)
for payment in pendingPayments:
await check_invoice_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
class CreateInvoiceData(BaseModel):
out: Optional[bool] = True
amount: int = Query(None, ge=1)
memo: str = None
unit: Optional[str] = "sat"
description_hash: Optional[str] = None
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
bolt11: Optional[str] = None
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.description_hash:
description_hash = unhexlify(data.description_hash)
memo = ""
else:
description_hash = b""
memo = g.data["memo"]
memo = data.memo
if data.unit == "sat":
amount = data.amount
else:
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
async with db.connect() as conn:
try:
payment_hash, payment_request = await create_invoice(
wallet_id=g.wallet.id,
amount=g.data["amount"],
wallet_id=wallet.id,
amount=amount,
memo=memo,
description_hash=description_hash,
extra=g.data.get("extra"),
webhook=g.data.get("webhook"),
extra=data.extra,
webhook=data.webhook,
conn=conn,
)
except InvoiceFailure as e:
return jsonify({"message": str(e)}), 520
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
if g.data.get("lnurl_callback"):
if "lnurl_balance_check" in g.data:
save_balance_check(g.wallet.id, g.data["lnurl_balance_check"])
if data.lnurl_callback:
if "lnurl_balance_check" in g().data:
save_balance_check(g().wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["lnurl_callback"],
data.lnurl_callback,
params={
"pr": payment_request,
"balanceNotify": url_for(
"core.lnurl_balance_notify",
service=urlparse(g.data["lnurl_callback"]).netloc,
wal=g.wallet.id,
_external=True,
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True,
wal=g().wallet.id,
),
},
timeout=10,
@@ -123,315 +154,350 @@ async def api_payments_create_invoice():
except (httpx.ConnectError, httpx.RequestError):
lnurl_response = False
return (
jsonify(
{
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"lnurl_response": lnurl_response,
}
),
HTTPStatus.CREATED,
)
return {
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"lnurl_response": lnurl_response,
}
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={"bolt11": {"type": "string", "empty": False, "required": True}}
)
async def api_payments_pay_invoice():
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
try:
payment_hash = await pay_invoice(
wallet_id=g.wallet.id,
payment_request=g.data["bolt11"],
)
payment_hash = await pay_invoice(wallet_id=wallet.id, payment_request=bolt11)
except ValueError as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except PermissionError as e:
return jsonify({"message": str(e)}), HTTPStatus.FORBIDDEN
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=str(e))
except PaymentFailure as e:
return jsonify({"message": str(e)}), 520
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
return (
jsonify(
{
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments", methods=["POST"])
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
async def api_payments_create():
if g.data["out"] is True:
return await api_payments_pay_invoice()
return await api_payments_create_invoice()
@core_app.route("/api/v1/payments/lnurl", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"description_hash": {"type": "string", "empty": False, "required": True},
"callback": {"type": "string", "empty": False, "required": True},
"amount": {"type": "number", "empty": False, "required": True},
"comment": {
"type": "string",
"nullable": True,
"empty": True,
"required": False,
},
"description": {
"type": "string",
"nullable": True,
"empty": True,
"required": False,
},
return {
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
@core_app.post(
"/api/v1/payments",
# deprecated=True,
# description="DEPRECATED. Use /api/v2/TBD and /api/v2/TBD instead",
status_code=HTTPStatus.CREATED,
)
async def api_payments_pay_lnurl():
domain = urlparse(g.data["callback"]).netloc
async def api_payments_create(
wallet: WalletTypeInfo = Depends(get_key_type),
invoiceData: CreateInvoiceData = Body(...),
):
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="BOLT11 string is invalid or not given",
)
return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet
) # admin key
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
class CreateLNURLData(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
@core_app.post("/api/v1/payments/lnurl")
async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(get_key_type)
):
domain = urlparse(data.callback).netloc
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["callback"],
params={"amount": g.data["amount"], "comment": g.data["comment"]},
data.callback,
params={"amount": data.amount, "comment": data.comment},
timeout=40,
)
if r.is_error:
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
raise httpx.ConnectError
except (httpx.ConnectError, httpx.RequestError):
return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Failed to connect to {domain}.",
)
params = json.loads(r.text)
if params.get("status") == "ERROR":
return (
jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}),
HTTPStatus.BAD_REQUEST,
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} said: '{params.get('reason', '')}'",
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != g.data["amount"]:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
}
),
HTTPStatus.BAD_REQUEST,
if invoice.amount_msat != data.amount:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected {data['amount']} msat, got {invoice.amount_msat}.",
)
if invoice.description_hash != g.data["description_hash"]:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
}
),
HTTPStatus.BAD_REQUEST,
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 = {}
if params.get("successAction"):
extra["success_action"] = params["successAction"]
if g.data["comment"]:
extra["comment"] = g.data["comment"]
if data.comment:
extra["comment"] = data.comment
payment_hash = await pay_invoice(
wallet_id=g.wallet.id,
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
description=g.data.get("description", ""),
description=data.description,
extra=extra,
)
return (
jsonify(
{
"success_action": params.get("successAction"),
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
HTTPStatus.CREATED,
return {
"success_action": params.get("successAction"),
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
async def subscribe(request: Request, wallet: Wallet):
this_wallet_id = wallet.wallet.id
payment_queue = asyncio.Queue(0)
print("adding sse listener", payment_queue)
api_invoice_listeners.append(payment_queue)
send_queue = asyncio.Queue(0)
async def payment_received() -> None:
while True:
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
await send_queue.put(("payment-received", payment))
asyncio.create_task(payment_received())
try:
while True:
typ, data = await send_queue.get()
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
# yield dict(id=1, event="this", data="1234")
# await asyncio.sleep(2)
yield dict(data=jdata, event=typ)
# yield dict(data=jdata.encode("utf-8"), event=typ.encode("utf-8"))
except asyncio.CancelledError:
return
@core_app.get("/api/v1/payments/sse")
async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
return EventSourceResponse(
subscribe(request, wallet), ping=20, media_type="text/event-stream"
)
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@api_check_wallet_key("invoice")
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash):
payment = await g.wallet.get_payment(payment_hash)
payment = await get_standalone_payment(payment_hash)
await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash)
if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
elif not payment.pending:
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
return {"paid": True, "preimage": payment.preimage}
try:
await payment.check_pending()
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
return {"paid": False}
return (
jsonify({"paid": not payment.pending, "preimage": payment.preimage}),
HTTPStatus.OK,
)
return {"paid": not payment.pending, "preimage": payment.preimage}
@core_app.route("/api/v1/payments/sse", methods=["GET"])
@api_check_wallet_key("invoice", accept_querystring=True)
async def api_payments_sse():
this_wallet_id = g.wallet.id
send_payment, receive_payment = trio.open_memory_channel(0)
print("adding sse listener", send_payment)
sse_listeners.append(send_payment)
send_event, event_to_send = trio.open_memory_channel(0)
async def payment_received() -> None:
async for payment in receive_payment:
if payment.wallet_id == this_wallet_id:
await send_event.send(("payment-received", payment))
async def repeat_keepalive():
await trio.sleep(1)
while True:
await send_event.send(("keepalive", ""))
await trio.sleep(25)
g.nursery.start_soon(payment_received)
g.nursery.start_soon(repeat_keepalive)
async def send_events():
try:
async for typ, data in event_to_send:
message = [f"event: {typ}".encode("utf-8")]
if data:
jdata = json.dumps(dict(data._asdict(), pending=False))
message.append(f"data: {jdata}".encode("utf-8"))
yield b"\n".join(message) + b"\r\n\r\n"
except trio.Cancelled:
return
response = await make_response(
send_events(),
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
"Transfer-Encoding": "chunked",
},
)
response.timeout = None
return response
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
@api_check_wallet_key("invoice")
@core_app.get(
"/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]
)
async def api_lnurlscan(code: str):
try:
url = lnurl.Lnurl(code)
except ValueError:
return jsonify({"message": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
domain = urlparse(url.url).netloc
url = lnurl.decode(code)
domain = urlparse(url).netloc
except:
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
name, domain = name_domain
url = (
("http://" if domain.endswith(".onion") else "https://")
+ domain
+ "/.well-known/lnurlp/"
+ name
)
# will proceed with these values
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
)
# params is what will be returned to the client
params: Dict = {"domain": domain}
if url.is_login:
if "tag=login" in url:
params.update(kind="auth")
params.update(callback=url.url) # with k1 already in it
params.update(callback=url) # with k1 already in it
lnurlauth_key = g.wallet.lnurlauth_key(domain)
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
lnurlauth_key = g().wallet.lnurlauth_key(domain)
params.update(
pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
else:
async with httpx.AsyncClient() as client:
r = await client.get(url.url, timeout=40)
r = await client.get(url, timeout=5)
if r.is_error:
return (
jsonify({"domain": domain, "message": "failed to get parameters"}),
HTTPStatus.SERVICE_UNAVAILABLE,
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={"domain": domain,
"message": "failed to get parameters"},
)
try:
jdata = json.loads(r.text)
data: lnurl.LnurlResponseModel = lnurl.LnurlResponse.from_dict(jdata)
except (json.decoder.JSONDecodeError, lnurl.exceptions.LnurlResponseException):
return (
jsonify(
{
data = json.loads(r.text)
except json.decoder.JSONDecodeError:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
},
)
try:
tag = data["tag"]
if tag == "channelRequest":
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail={
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
}
),
HTTPStatus.SERVICE_UNAVAILABLE,
"kind": "channel",
"message": "unsupported",
},
)
params.update(**data)
if tag == "withdrawRequest":
params.update(kind="withdraw")
params.update(fixed=data["minWithdrawable"]
== data["maxWithdrawable"])
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data["callback"])
qs: Dict = parse_qs(parsed_callback.query)
qs["k1"] = data["k1"]
# balanceCheck/balanceNotify
if "balanceCheck" in data:
params.update(balanceCheck=data["balanceCheck"])
# format callback url and send to client
parsed_callback = parsed_callback._replace(
query=urlencode(qs, doseq=True)
)
params.update(callback=urlunparse(parsed_callback))
if tag == "payRequest":
params.update(kind="pay")
params.update(fixed=data["minSendable"] == data["maxSendable"])
params.update(
description_hash=hashlib.sha256(
data["metadata"].encode("utf-8")
).hexdigest()
)
metadata = json.loads(data["metadata"])
for [k, v] in metadata:
if k == "text/plain":
params.update(description=v)
if k == "image/jpeg;base64" or k == "image/png;base64":
data_uri = "data:" + k + "," + v
params.update(image=data_uri)
if k == "text/email" or k == "text/identifier":
params.update(targetUser=v)
params.update(commentAllowed=data.get("commentAllowed", 0))
except KeyError as exc:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"lnurl JSON response invalid: {exc}",
},
)
if type(data) is lnurl.LnurlChannelResponse:
return (
jsonify(
{"domain": domain, "kind": "channel", "message": "unsupported"}
),
HTTPStatus.BAD_REQUEST,
)
params.update(**data.dict())
if type(data) is lnurl.LnurlWithdrawResponse:
params.update(kind="withdraw")
params.update(fixed=data.min_withdrawable == data.max_withdrawable)
# callback with k1 already in it
parsed_callback: ParseResult = urlparse(data.callback)
qs: Dict = parse_qs(parsed_callback.query)
qs["k1"] = data.k1
# balanceCheck/balanceNotify
if "balanceCheck" in jdata:
params.update(balanceCheck=jdata["balanceCheck"])
# format callback url and send to client
parsed_callback = parsed_callback._replace(query=urlencode(qs, doseq=True))
params.update(callback=urlunparse(parsed_callback))
if type(data) is lnurl.LnurlPayResponse:
params.update(kind="pay")
params.update(fixed=data.min_sendable == data.max_sendable)
params.update(description_hash=data.metadata.h)
params.update(description=data.metadata.text)
if data.metadata.images:
image = min(data.metadata.images, key=lambda image: len(image[1]))
data_uri = "data:" + image[0] + "," + image[1]
params.update(image=data_uri)
params.update(commentAllowed=jdata.get("commentAllowed", 0))
return jsonify(params)
return params
@core_app.route("/api/v1/lnurlauth", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"callback": {"type": "string", "required": True},
}
)
async def api_perform_lnurlauth():
err = await perform_lnurlauth(g.data["callback"])
@core_app.post("/api/v1/payments/decode")
async def api_payments_decode(data: str = Query(None)):
try:
if data["data"][:5] == "LNURL":
url = lnurl.decode(data["data"])
return {"domain": url}
else:
invoice = bolt11.decode(data["data"])
return {
"payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat,
"description": invoice.description,
"description_hash": invoice.description_hash,
"payee": invoice.payee,
"date": invoice.date,
"expiry": invoice.expiry,
"secret": invoice.secret,
"route_hints": invoice.route_hints,
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
}
except:
return {"message": "Failed to decode"}
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback)
if err:
return jsonify({"reason": err.reason}), HTTPStatus.SERVICE_UNAVAILABLE
return "", HTTPStatus.OK
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
)
return ""
@core_app.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())
+189 -156
View File
@@ -1,245 +1,278 @@
from os import path
import asyncio
from http import HTTPStatus
from quart import (
g,
abort,
jsonify,
request,
redirect,
render_template,
send_from_directory,
url_for,
)
from typing import Optional
from lnbits.core import core_app, db
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.settings import LNBITS_ALLOWED_USERS, SERVICE_FEE, LNBITS_SITE_TITLE
from fastapi import Request, status
from fastapi.exceptions import HTTPException
from fastapi.params import Depends, Query
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.routing import APIRouter
from pydantic.types import UUID4
from starlette.responses import HTMLResponse, JSONResponse
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 ..crud import (
create_account,
get_user,
update_user_extension,
create_wallet,
delete_wallet,
get_balance_check,
get_user,
save_balance_notify,
update_user_extension,
)
from ..services import redeem_lnurl_withdraw, pay_invoice
from ..services import pay_invoice, redeem_lnurl_withdraw
core_html_routes: APIRouter = APIRouter(tags=["Core NON-API Website Routes"])
@core_app.route("/favicon.ico")
@core_html_routes.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return await send_from_directory(
path.join(core_app.root_path, "static"), "favicon.ico"
return FileResponse("lnbits/core/static/favicon.ico")
@core_html_routes.get("/", response_class=HTMLResponse)
async def home(request: Request, lightning: str = None):
return template_renderer().TemplateResponse(
"core/index.html", {"request": request, "lnurl": lightning}
)
@core_app.route("/")
async def home():
return await render_template(
"core/index.html", lnurl=request.args.get("lightning", None)
)
@core_app.route("/extensions")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def extensions():
extension_to_enable = request.args.get("enable", type=str)
extension_to_disable = request.args.get("disable", type=str)
@core_html_routes.get(
"/extensions",
name="core.extensions",
response_class=HTMLResponse,
)
async def extensions(
request: Request,
user: User = Depends(check_user_exists),
enable: str = Query(None),
disable: str = Query(None),
):
extension_to_enable = enable
extension_to_disable = disable
if extension_to_enable and extension_to_disable:
abort(
raise HTTPException(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
if extension_to_enable:
await update_user_extension(
user_id=g.user.id, extension=extension_to_enable, active=1
user_id=user.id, extension=extension_to_enable, active=True
)
elif extension_to_disable:
await update_user_extension(
user_id=g.user.id, extension=extension_to_disable, active=0
user_id=user.id, extension=extension_to_disable, active=False
)
return await render_template("core/extensions.html", user=await get_user(g.user.id))
# Update user as his extensions have been updated
if extension_to_enable or extension_to_disable:
user = await get_user(user.id)
return template_renderer().TemplateResponse(
"core/extensions.html", {"request": request, "user": user.dict()}
)
@core_app.route("/wallet")
@validate_uuids(["usr", "wal"])
async def wallet():
user_id = request.args.get("usr", type=str)
wallet_id = request.args.get("wal", type=str)
wallet_name = request.args.get("nme", type=str)
@core_html_routes.get(
"/wallet",
response_class=HTMLResponse,
description="""
Args:
just **wallet_name**: create a new user, then create a new wallet for user with wallet_name<br>
just **user_id**: return the first user wallet or create one if none found (with default wallet_name)<br>
**user_id** and **wallet_name**: create a new wallet for user with wallet_name<br>
**user_id** and **wallet_id**: return that wallet if user is the owner<br>
nothing: create everything<br>
""",
)
async def wallet(
request: Request = Query(None),
nme: Optional[str] = Query(None),
usr: Optional[UUID4] = Query(None),
wal: Optional[UUID4] = Query(None),
):
user_id = usr.hex if usr else None
wallet_id = wal.hex if wal else None
wallet_name = nme
service_fee = int(SERVICE_FEE) if int(SERVICE_FEE) == SERVICE_FEE else SERVICE_FEE
# just wallet_name: create a new user, then create a new wallet for user with wallet_name
# just user_id: return the first user wallet or create one if none found (with default wallet_name)
# user_id and wallet_name: create a new wallet for user with wallet_name
# user_id and wallet_id: return that wallet if user is the owner
# nothing: create everything
if not user_id:
user = await get_user((await create_account()).id)
else:
user = await get_user(user_id)
if not user:
abort(HTTPStatus.NOT_FOUND, "User does not exist.")
return
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User does not exist."}
)
if LNBITS_ALLOWED_USERS and user_id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User not authorized."}
)
if not wallet_id:
if user.wallets and not wallet_name:
wallet = user.wallets[0]
else:
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
wallet = user.get_wallet(wallet_id)
if not wallet:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
return await render_template(
"core/wallet.html", user=user, wallet=wallet, service_fee=service_fee
)
@core_app.route("/withdraw")
@validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw():
user = await get_user(request.args.get("usr"))
if not user:
return jsonify({"status": "ERROR", "reason": "User does not exist."})
wallet = user.get_wallet(request.args.get("wal"))
if not wallet:
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
return jsonify(
return template_renderer().TemplateResponse(
"core/wallet.html",
{
"tag": "withdrawRequest",
"callback": url_for(
"core.lnurl_full_withdraw_callback",
usr=user.id,
wal=wallet.id,
_external=True,
),
"k1": "0",
"minWithdrawable": 1 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
"balanceCheck": url_for(
"core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True
),
}
"request": request,
"user": user.dict(),
"wallet": wallet.dict(),
"service_fee": service_fee,
},
)
@core_app.route("/withdraw/cb")
@validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw_callback():
user = await get_user(request.args.get("usr"))
@core_html_routes.get("/withdraw", response_class=JSONResponse)
async def lnurl_full_withdraw(request: Request):
user = await get_user(request.query_params.get("usr"))
if not user:
return jsonify({"status": "ERROR", "reason": "User does not exist."})
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.args.get("wal"))
wallet = user.get_wallet(request.query_params.get("wal"))
if not wallet:
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
return {"status": "ERROR", "reason": "Wallet does not exist."}
pr = request.args.get("pr")
return {
"tag": "withdrawRequest",
"callback": url_for("/withdraw/cb", external=True, usr=user.id, wal=wallet.id),
"k1": "0",
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": f"{LNBITS_SITE_TITLE} balance withdraw from {wallet.id[0:5]}",
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
}
@core_html_routes.get("/withdraw/cb", response_class=JSONResponse)
async def lnurl_full_withdraw_callback(request: Request):
user = await get_user(request.query_params.get("usr"))
if not user:
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.query_params.get("wal"))
if not wallet:
return {"status": "ERROR", "reason": "Wallet does not exist."}
pr = request.query_params.get("pr")
async def pay():
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
try:
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
except:
pass
g.nursery.start_soon(pay)
asyncio.create_task(pay())
balance_notify = request.args.get("balanceNotify")
balance_notify = request.query_params.get("balanceNotify")
if balance_notify:
await save_balance_notify(wallet.id, balance_notify)
return jsonify({"status": "OK"})
return {"status": "OK"}
@core_app.route("/deletewallet")
@validate_uuids(["usr", "wal"], required=True)
@check_user_exists()
async def deletewallet():
wallet_id = request.args.get("wal", type=str)
user_wallet_ids = g.user.wallet_ids
@core_html_routes.get("/deletewallet", response_class=RedirectResponse)
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
user = await get_user(usr)
user_wallet_ids = [u.id for u in user.wallets]
print("USR", user_wallet_ids)
if wallet_id not in user_wallet_ids:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
if wal not in user_wallet_ids:
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
else:
await delete_wallet(user_id=g.user.id, wallet_id=wallet_id)
user_wallet_ids.remove(wallet_id)
await delete_wallet(user_id=user.id, wallet_id=wal)
user_wallet_ids.remove(wal)
if user_wallet_ids:
return redirect(url_for("core.wallet", usr=g.user.id, wal=user_wallet_ids[0]))
return RedirectResponse(
url_for("/wallet", usr=user.id, wal=user_wallet_ids[0]),
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
return redirect(url_for("core.home"))
return RedirectResponse(
url_for("/"), status_code=status.HTTP_307_TEMPORARY_REDIRECT
)
@core_app.route("/withdraw/notify/<service>")
@validate_uuids(["wal"], required=True)
async def lnurl_balance_notify(service: str):
bc = await get_balance_check(request.args.get("wal"), service)
@core_html_routes.get("/withdraw/notify/{service}")
async def lnurl_balance_notify(request: Request, service: str):
bc = await get_balance_check(request.query_params.get("wal"), service)
if bc:
redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_app.route("/lnurlwallet")
async def lnurlwallet():
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse)
async def lnurlwallet(request: Request):
async with db.connect() as conn:
account = await create_account(conn=conn)
user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn)
g.nursery.start_soon(
redeem_lnurl_withdraw,
wallet.id,
request.args.get("lightning"),
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service
asyncio.create_task(
redeem_lnurl_withdraw(
wallet.id,
request.query_params.get("lightning"),
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
5, # wait 5 seconds before sending the invoice to the service
)
)
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@core_app.route("/manifest/<usr>.webmanifest")
@core_html_routes.get("/manifest/{usr}.webmanifest")
async def manifest(usr: str):
user = await get_user(usr)
if not user:
return "", HTTPStatus.NOT_FOUND
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return jsonify(
{
"short_name": "LNbits",
"name": "LNbits Wallet",
"icons": [
{
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr,
"background_color": "#3367D6",
"description": "Weather forecast information",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
}
for wallet in user.wallets
],
}
)
return {
"short_name": "LNbits",
"name": "LNbits Wallet",
"icons": [
{
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": "/wallet?usr=" + usr,
"background_color": "#3367D6",
"description": "Weather forecast information",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
}
for wallet in user.wallets
],
}
+70
View File
@@ -0,0 +1,70 @@
import asyncio
import datetime
from http import HTTPStatus
from urllib.parse import urlparse
from fastapi import HTTPException
from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits import bolt11
from .. import core_app
from ..crud import get_standalone_payment
from ..tasks import api_invoice_listeners
@core_app.get("/.well-known/lnurlp/{username}")
async def lnaddress(username: str, request: Request):
from lnbits.extensions.lnaddress.lnurl import lnurl_response
domain = urlparse(str(request.url)).netloc
return await lnurl_response(username, domain, request)
@core_app.get("/public/v1/payment/{payment_hash}")
async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash)
if not payment:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
elif not payment.pending:
return {"status": "paid"}
try:
invoice = bolt11.decode(payment.bolt11)
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration < datetime.datetime.now():
return {"status": "expired"}
except:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Invalid bolt11 invoice."
)
payment_queue = asyncio.Queue(0)
print("adding standalone invoice listener", payment_hash, payment_queue)
api_invoice_listeners.append(payment_queue)
response = None
async def payment_info_receiver(cancel_scope):
async for payment in payment_queue.get():
if payment.payment_hash == payment_hash:
nonlocal response
response = {"status": "paid"}
cancel_scope.cancel()
async def timeouter(cancel_scope):
await asyncio.sleep(45)
cancel_scope.cancel()
asyncio.create_task(payment_info_receiver())
asyncio.create_task(timeouter())
if response:
return response
else:
raise HTTPException(status_code=HTTPStatus.REQUEST_TIMEOUT, detail="timeout")
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
+138 -16
View File
@@ -1,45 +1,167 @@
import asyncio
import datetime
import os
import trio
import time
from contextlib import asynccontextmanager
from sqlalchemy import create_engine # type: ignore
from sqlalchemy_aio import TRIO_STRATEGY # type: ignore
from typing import Optional
from sqlalchemy import create_engine
from sqlalchemy_aio.base import AsyncConnection
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
from .settings import LNBITS_DATA_FOLDER
from .settings import LNBITS_DATA_FOLDER, LNBITS_DATABASE_URL
POSTGRES = "POSTGRES"
COCKROACH = "COCKROACH"
SQLITE = "SQLITE"
class Connection:
def __init__(self, conn: AsyncConnection):
class Compat:
type: Optional[str] = "<inherited>"
schema: Optional[str] = "<inherited>"
def interval_seconds(self, seconds: int) -> str:
if self.type in {POSTGRES, COCKROACH}:
return f"interval '{seconds} seconds'"
elif self.type == SQLITE:
return f"{seconds}"
return "<nothing>"
@property
def timestamp_now(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
return "now()"
elif self.type == SQLITE:
return "(strftime('%s', 'now'))"
return "<nothing>"
@property
def serial_primary_key(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
return "SERIAL PRIMARY KEY"
elif self.type == SQLITE:
return "INTEGER PRIMARY KEY AUTOINCREMENT"
return "<nothing>"
@property
def references_schema(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
return f"{self.schema}."
elif self.type == SQLITE:
return ""
return "<nothing>"
class Connection(Compat):
def __init__(self, conn: AsyncConnection, txn, typ, name, schema):
self.conn = conn
self.txn = txn
self.type = typ
self.name = name
self.schema = schema
def rewrite_query(self, query) -> str:
if self.type in {POSTGRES, COCKROACH}:
query = query.replace("%", "%%")
query = query.replace("?", "%s")
return query
async def fetchall(self, query: str, values: tuple = ()) -> list:
result = await self.conn.execute(query, values)
result = await self.conn.execute(self.rewrite_query(query), values)
return await result.fetchall()
async def fetchone(self, query: str, values: tuple = ()):
result = await self.conn.execute(query, values)
result = await self.conn.execute(self.rewrite_query(query), values)
row = await result.fetchone()
await result.close()
return row
async def execute(self, query: str, values: tuple = ()):
return await self.conn.execute(query, values)
return await self.conn.execute(self.rewrite_query(query), values)
class Database:
class Database(Compat):
def __init__(self, db_name: str):
self.db_name = db_name
db_path = os.path.join(LNBITS_DATA_FOLDER, f"{db_name}.sqlite3")
self.engine = create_engine(f"sqlite:///{db_path}", strategy=TRIO_STRATEGY)
self.lock = trio.StrictFIFOLock()
self.name = db_name
if LNBITS_DATABASE_URL:
database_uri = LNBITS_DATABASE_URL
if database_uri.startswith("cockroachdb://"):
self.type = COCKROACH
else:
self.type = POSTGRES
import psycopg2 # type: ignore
def _parse_timestamp(value, _):
f = "%Y-%m-%d %H:%M:%S.%f"
if not "." in value:
f = "%Y-%m-%d %H:%M:%S"
return time.mktime(datetime.datetime.strptime(value, f).timetuple())
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
psycopg2.extensions.DECIMAL.values,
"DEC2FLOAT",
lambda value, curs: float(value) if value is not None else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1082, 1083, 1266),
"DATE2INT",
lambda value, curs: time.mktime(value.timetuple())
if value is not None
else None,
)
)
psycopg2.extensions.register_type(
psycopg2.extensions.new_type(
(1184, 1114),
"TIMESTAMP2INT",
_parse_timestamp
# lambda value, curs: time.mktime(
# datetime.datetime.strptime(
# value, "%Y-%m-%d %H:%M:%S.%f"
# ).timetuple()
# ),
)
)
else:
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
self.schema = self.name
if self.name.startswith("ext_"):
self.schema = self.name[4:]
else:
self.schema = None
self.engine = create_engine(database_uri, strategy=ASYNCIO_STRATEGY)
self.lock = asyncio.Lock()
@asynccontextmanager
async def connect(self):
await self.lock.acquire()
try:
async with self.engine.connect() as conn:
async with conn.begin():
yield Connection(conn)
async with conn.begin() as txn:
wconn = Connection(conn, txn, self.type, self.name, self.schema)
if self.schema:
if self.type in {POSTGRES, COCKROACH}:
await wconn.execute(
f"CREATE SCHEMA IF NOT EXISTS {self.schema}"
)
elif self.type == SQLITE:
await wconn.execute(
f"ATTACH '{self.path}' AS {self.schema}"
)
yield wconn
finally:
self.lock.release()
+166 -83
View File
@@ -1,104 +1,187 @@
from cerberus import Validator # type: ignore
from quart import g, abort, jsonify, request
from functools import wraps
from http import HTTPStatus
from typing import List, Union
from uuid import UUID
from cerberus import Validator # type: ignore
from fastapi import status
from fastapi.exceptions import HTTPException
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.params import Security
from fastapi.security.api_key import APIKeyHeader, APIKeyQuery
from fastapi.security.base import SecurityBase
from pydantic.types import UUID4
from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.requestvars import g
from lnbits.settings import LNBITS_ALLOWED_USERS
def api_check_wallet_key(key_type: str = "invoice", accept_querystring=False):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
try:
key_value = request.headers.get("X-Api-Key") or request.args["api-key"]
g.wallet = await get_wallet_for_key(key_value, key_type)
except KeyError:
return (
jsonify({"message": "`X-Api-Key` header missing."}),
HTTPStatus.BAD_REQUEST,
class KeyChecker(SecurityBase):
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
self._key_type = "invoice"
self._api_key = api_key
if api_key:
self.model: APIKey = APIKey(
**{"in": APIKeyIn.query},
name="X-API-KEY",
description="Wallet API Key - QUERY",
)
else:
self.model: APIKey = APIKey(
**{"in": APIKeyIn.header},
name="X-API-KEY",
description="Wallet API Key - HEADER",
)
self.wallet = None
async def __call__(self, request: Request) -> Wallet:
try:
key_value = (
self._api_key
if self._api_key
else request.headers.get("X-API-KEY") or request.query_params["api-key"]
)
# FIXME: Find another way to validate the key. A fetch from DB should be avoided here.
# Also, we should not return the wallet here - thats silly.
# Possibly store it in a Redis DB
self.wallet = await get_wallet_for_key(key_value, self._key_type)
if not self.wallet:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invalid key or expired key.",
)
if not g.wallet:
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
return await view(**kwargs)
return wrapped_view
return wrap
def api_validate_post_request(*, schema: dict):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]:
return (
jsonify({"message": "Content-Type must be `application/json`."}),
HTTPStatus.BAD_REQUEST,
)
v = Validator(schema)
data = await request.get_json()
g.data = {key: data[key] for key in schema.keys() if key in data}
if not v.validate(g.data):
return (
jsonify({"message": f"Errors in request data: {v.errors}"}),
HTTPStatus.BAD_REQUEST,
)
return await view(**kwargs)
return wrapped_view
return wrap
def check_user_exists(param: str = "usr"):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
g.user = await get_user(request.args.get(param, type=str)) or abort(
HTTPStatus.NOT_FOUND, "User does not exist."
except KeyError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="`X-API-KEY` header missing."
)
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
return await view(**kwargs)
class WalletInvoiceKeyChecker(KeyChecker):
"""
WalletInvoiceKeyChecker will ensure that the provided invoice
wallet key is correct and populate g().wallet with the wallet
for the key in `X-API-key`.
return wrapped_view
The checker will raise an HTTPException when the key is wrong in some ways.
"""
return wrap
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "invoice"
def validate_uuids(
params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4
class WalletAdminKeyChecker(KeyChecker):
"""
WalletAdminKeyChecker will ensure that the provided admin
wallet key is correct and populate g().wallet with the wallet
for the key in `X-API-key`.
The checker will raise an HTTPException when the key is wrong in some ways.
"""
def __init__(
self, scheme_name: str = None, auto_error: bool = True, api_key: str = None
):
super().__init__(scheme_name, auto_error, api_key)
self._key_type = "admin"
class WalletTypeInfo:
wallet_type: int
wallet: Wallet
def __init__(self, wallet_type: int, wallet: Wallet) -> None:
self.wallet_type = wallet_type
self.wallet = wallet
api_key_header = APIKeyHeader(
name="X-API-KEY",
auto_error=False,
description="Admin or Invoice key for wallet API's",
)
api_key_query = APIKeyQuery(
name="api-key",
auto_error=False,
description="Admin or Invoice key for wallet API's",
)
async def get_key_type(
r: Request,
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
) -> WalletTypeInfo:
# 0: admin
# 1: invoice
# 2: invalid
if not api_key_header and not api_key_query:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
token = api_key_header if api_key_header else api_key_query
try:
checker = WalletAdminKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(0, checker.wallet)
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise
if e.status_code == HTTPStatus.UNAUTHORIZED:
pass
except:
raise
try:
checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(1, checker.wallet)
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise
if e.status_code == HTTPStatus.UNAUTHORIZED:
return WalletTypeInfo(2, None)
except:
raise
async def require_admin_key(
r: Request,
api_key_header: str = Security(api_key_header),
api_key_query: str = Security(api_key_query),
):
def wrap(view):
@wraps(view)
async def wrapped_view(**kwargs):
query_params = {
param: request.args.get(param, type=str) for param in params
}
token = api_key_header if api_key_header else api_key_query
for param, value in query_params.items():
if not value and (required is True or (required and param in required)):
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is required.")
wallet = await get_key_type(r, token)
if value:
try:
UUID(value, version=version)
except ValueError:
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
if wallet.wallet_type != 0:
# If wallet type is not admin then return the unauthorized status
# This also covers when the user passes an invalid key type
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Admin key required."
)
else:
return wallet
return await view(**kwargs)
return wrapped_view
async def check_user_exists(usr: UUID4) -> User:
g().user = await get_user(usr.hex)
if not g().user:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
)
return wrap
if LNBITS_ALLOWED_USERS and g().user.id not in LNBITS_ALLOWED_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return g().user
-11
View File
@@ -1,11 +0,0 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
This is an example extension to help you organise and build you own.
Try to include an image
<img src="https://i.imgur.com/9i4xcQB.png">
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
-12
View File
@@ -1,12 +0,0 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_amilk")
amilk_ext: Blueprint = Blueprint(
"amilk", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
-6
View File
@@ -1,6 +0,0 @@
{
"name": "AMilk",
"short_description": "Assistant Faucet Milker",
"icon": "room_service",
"contributors": ["arcbtc"]
}
-42
View File
@@ -1,42 +0,0 @@
from base64 import urlsafe_b64encode
from uuid import uuid4
from typing import List, Optional, Union
from . import db
from .models import AMilk
async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk:
amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
await db.execute(
"""
INSERT INTO amilks (id, wallet, lnurl, atime, amount)
VALUES (?, ?, ?, ?, ?)
""",
(amilk_id, wallet_id, lnurl, atime, amount),
)
amilk = await get_amilk(amilk_id)
assert amilk, "Newly created amilk_id couldn't be retrieved"
return amilk
async def get_amilk(amilk_id: str) -> Optional[AMilk]:
row = await db.fetchone("SELECT * FROM amilks WHERE id = ?", (amilk_id,))
return AMilk(**row) if row else None
async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM amilks WHERE wallet IN ({q})", (*wallet_ids,)
)
return [AMilk(**row) for row in rows]
async def delete_amilk(amilk_id: str) -> None:
await db.execute("DELETE FROM amilks WHERE id = ?", (amilk_id,))
-15
View File
@@ -1,15 +0,0 @@
async def m001_initial(db):
"""
Initial amilks table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS amilks (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
lnurl TEXT NOT NULL,
atime INTEGER NOT NULL,
amount INTEGER NOT NULL
);
"""
)
-9
View File
@@ -1,9 +0,0 @@
from typing import NamedTuple
class AMilk(NamedTuple):
id: str
wallet: str
lnurl: str
atime: int
amount: int
@@ -1,24 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">Assistant Faucet Milker</h5>
<p>
Milking faucets with software, known as "assmilking", seems at first to
be black-hat, although in fact there might be some unexplored use cases.
An LNURL withdraw gives someone the right to pull funds, which can be
done over time. An LNURL withdraw could be used outside of just faucets,
to provide money streaming and repeat payments.<br />Paste or scan an
LNURL withdraw, enter the amount for the AMilk to pull and the frequency
for it to be pulled.<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
@@ -1,250 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="amilkDialog.show = true"
>New AMilk</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">AMilks</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="amilks"
row-key="id"
:columns="amilksTable.columns"
:pagination.sync="amilksTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteAMilk(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
LNbits Assistant Faucet Milker Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "amilk/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="amilkDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createAMilk" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="amilkDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="amilkDialog.data.lnurl"
type="url"
label="LNURL Withdraw"
></q-input>
<q-input
filled
dense
v-model.number="amilkDialog.data.amount"
type="number"
label="Amount *"
></q-input>
<q-input
filled
dense
v-model.trim="amilkDialog.data.atime"
type="number"
label="Hit frequency (secs)"
placeholder="Frequency to be hit"
></q-input>
<q-btn
unelevated
color="deep-purple"
:disable="amilkDialog.data.amount == null || amilkDialog.data.amount < 0 || amilkDialog.data.lnurl == null"
type="submit"
>Create amilk</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapAMilk = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.wall = ['/amilk/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
amilks: [],
amilksTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'lnurl', align: 'left', label: 'LNURL', field: 'lnurl'},
{name: 'atime', align: 'left', label: 'Freq', field: 'atime'},
{name: 'amount', align: 'left', label: 'Amount', field: 'amount'}
],
pagination: {
rowsPerPage: 10
}
},
amilkDialog: {
show: false,
data: {}
}
}
},
methods: {
getAMilks: function () {
var self = this
LNbits.api
.request(
'GET',
'/amilk/api/v1/amilk?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.amilks = response.data.map(function (obj) {
response.data.forEach(MILK)
function MILK(item) {
window.setInterval(function () {
LNbits.api
.request(
'GET',
'/amilk/api/v1/amilk/milk/' + item.id,
'Lorem'
)
.then(function (response) {
self.amilks = response.data.map(function (obj) {
return mapAMilk(obj)
})
})
}, item.atime * 1000)
}
return mapAMilk(obj)
})
})
},
createAMilk: function () {
var data = {
lnurl: this.amilkDialog.data.lnurl,
atime: parseInt(this.amilkDialog.data.atime),
amount: this.amilkDialog.data.amount
}
var self = this
console.log(this.amilkDialog.data.wallet)
LNbits.api
.request(
'POST',
'/amilk/api/v1/amilk',
_.findWhere(this.g.user.wallets, {id: this.amilkDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.amilks.push(mapAMilk(response.data))
self.amilkDialog.show = false
self.amilkDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteAMilk: function (amilkId) {
var self = this
var amilk = _.findWhere(this.amilks, {id: amilkId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this AMilk link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/amilk/api/v1/amilks/' + amilkId,
_.findWhere(self.g.user.wallets, {id: amilk.wallet}).inkey
)
.then(function (response) {
self.amilks = _.reject(self.amilks, function (obj) {
return obj.id == amilkId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.amilksTable.columns, this.amilks)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getAMilks()
}
}
})
</script>
{% endblock %}
-23
View File
@@ -1,23 +0,0 @@
from quart import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from . import amilk_ext
from .crud import get_amilk
@amilk_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("amilk/index.html", user=g.user)
@amilk_ext.route("/<amilk_id>")
async def wall(amilk_id):
amilk = await get_amilk(amilk_id)
if not amilk:
abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
return await render_template("amilk/wall.html", amilk=amilk)
-105
View File
@@ -1,105 +0,0 @@
import httpx
from quart import g, jsonify, request, abort
from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
from lnurl.exceptions import LnurlException # type: ignore
from time import sleep
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.core.services import create_invoice, check_invoice_status
from . import amilk_ext
from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
@amilk_ext.route("/api/v1/amilk", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_amilks():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return (
jsonify([amilk._asdict() for amilk in await get_amilks(wallet_ids)]),
HTTPStatus.OK,
)
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
async def api_amilkit(amilk_id):
milk = await get_amilk(amilk_id)
memo = milk.id
try:
withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse)
except LnurlException:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
try:
payment_hash, payment_request = await create_invoice(
wallet_id=milk.wallet,
amount=withdraw_res.max_sats,
memo=memo,
extra={"tag": "amilk"},
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
r = httpx.get(
withdraw_res.callback.base,
params={
**withdraw_res.callback.query_params,
**{"k1": withdraw_res.k1, "pr": payment_request},
},
)
if r.is_error:
abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.")
for i in range(10):
sleep(i)
invoice_status = await check_invoice_status(milk.wallet, payment_hash)
if invoice_status.paid:
return jsonify({"paid": True}), HTTPStatus.OK
else:
continue
return jsonify({"paid": False}), HTTPStatus.OK
@amilk_ext.route("/api/v1/amilk", methods=["POST"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"lnurl": {"type": "string", "empty": False, "required": True},
"atime": {"type": "integer", "min": 0, "required": True},
"amount": {"type": "integer", "min": 0, "required": True},
}
)
async def api_amilk_create():
amilk = await create_amilk(
wallet_id=g.wallet.id,
lnurl=g.data["lnurl"],
atime=g.data["atime"],
amount=g.data["amount"],
)
return jsonify(amilk._asdict()), HTTPStatus.CREATED
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
async def api_amilk_delete(amilk_id):
amilk = await get_amilk(amilk_id)
if not amilk:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if amilk.wallet != g.wallet.id:
return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN
await delete_amilk(amilk_id)
return "", HTTPStatus.NO_CONTENT
+19 -5
View File
@@ -1,12 +1,26 @@
from quart import Blueprint
from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_bleskomat")
bleskomat_ext: Blueprint = Blueprint(
"bleskomat", __name__, static_folder="static", template_folder="templates"
)
bleskomat_static_files = [
{
"path": "/bleskomat/static",
"app": StaticFiles(directory="lnbits/extensions/bleskomat/static"),
"name": "bleskomat_static",
}
]
bleskomat_ext: APIRouter = APIRouter(prefix="/bleskomat", tags=["Bleskomat"])
def bleskomat_renderer():
return template_renderer(["lnbits/extensions/bleskomat/templates"])
from .lnurl_api import * # noqa
from .views_api import * # noqa
from .views import * # noqa
from .views_api import * # noqa
+24 -23
View File
@@ -1,27 +1,21 @@
import secrets
import time
from uuid import uuid4
from typing import List, Optional, Union
from uuid import uuid4
from . import db
from .models import Bleskomat, BleskomatLnurl
from .helpers import generate_bleskomat_lnurl_hash
from .models import Bleskomat, BleskomatLnurl, CreateBleskomat
async def create_bleskomat(
*,
wallet_id: str,
name: str,
fiat_currency: str,
exchange_rate_provider: str,
fee: str,
) -> Bleskomat:
async def create_bleskomat(data: CreateBleskomat, wallet_id: str) -> Bleskomat:
bleskomat_id = uuid4().hex
api_key_id = secrets.token_hex(8)
api_key_secret = secrets.token_hex(32)
api_key_encoding = "hex"
await db.execute(
"""
INSERT INTO bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
INSERT INTO bleskomat.bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
@@ -30,10 +24,10 @@ async def create_bleskomat(
api_key_id,
api_key_secret,
api_key_encoding,
name,
fiat_currency,
exchange_rate_provider,
fee,
data.name,
data.fiat_currency,
data.exchange_rate_provider,
data.fee,
),
)
bleskomat = await get_bleskomat(bleskomat_id)
@@ -42,13 +36,15 @@ async def create_bleskomat(
async def get_bleskomat(bleskomat_id: str) -> Optional[Bleskomat]:
row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,))
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
)
return Bleskomat(**row) if row else None
async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]:
row = await db.fetchone(
"SELECT * FROM bleskomats WHERE api_key_id = ?", (api_key_id,)
"SELECT * FROM bleskomat.bleskomats WHERE api_key_id = ?", (api_key_id,)
)
return Bleskomat(**row) if row else None
@@ -58,7 +54,7 @@ async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]:
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM bleskomats WHERE wallet IN ({q})", (*wallet_ids,)
f"SELECT * FROM bleskomat.bleskomats WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Bleskomat(**row) for row in rows]
@@ -66,14 +62,17 @@ async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]:
async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE bleskomats SET {q} WHERE id = ?", (*kwargs.values(), bleskomat_id)
f"UPDATE bleskomat.bleskomats SET {q} WHERE id = ?",
(*kwargs.values(), bleskomat_id),
)
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)
)
row = await db.fetchone("SELECT * FROM bleskomats WHERE id = ?", (bleskomat_id,))
return Bleskomat(**row) if row else None
async def delete_bleskomat(bleskomat_id: str) -> None:
await db.execute("DELETE FROM bleskomats WHERE id = ?", (bleskomat_id,))
await db.execute("DELETE FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,))
async def create_bleskomat_lnurl(
@@ -84,7 +83,7 @@ async def create_bleskomat_lnurl(
now = int(time.time())
await db.execute(
"""
INSERT INTO bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time)
INSERT INTO bleskomat.bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
@@ -108,5 +107,7 @@ async def create_bleskomat_lnurl(
async def get_bleskomat_lnurl(secret: str) -> Optional[BleskomatLnurl]:
hash = generate_bleskomat_lnurl_hash(secret)
row = await db.fetchone("SELECT * FROM bleskomat_lnurls WHERE hash = ?", (hash,))
row = await db.fetchone(
"SELECT * FROM bleskomat.bleskomat_lnurls WHERE hash = ?", (hash,)
)
return BleskomatLnurl(**row) if row else None
+10 -9
View File
@@ -65,15 +65,16 @@ async def fetch_fiat_exchange_rate(currency: str, provider: str):
}
url = exchange_rate_providers[provider]["api_url"]
for key in replacements.keys():
url = url.replace("{" + key + "}", replacements[key])
if url:
for key in replacements.keys():
url = url.replace("{" + key + "}", replacements[key])
async with httpx.AsyncClient() as client:
r = await client.get(url)
r.raise_for_status()
data = r.json()
else:
data = {}
getter = exchange_rate_providers[provider]["getter"]
async with httpx.AsyncClient() as client:
r = await client.get(url)
r.raise_for_status()
data = r.json()
rate = float(getter(data, replacements))
rate = float(getter(data, replacements))
return rate
+7 -6
View File
@@ -1,11 +1,12 @@
import base64
import hashlib
import hmac
from http import HTTPStatus
from binascii import unhexlify
from typing import Dict
from quart import url_for
import urllib
from binascii import unhexlify
from http import HTTPStatus
from typing import Dict
from starlette.requests import Request
def generate_bleskomat_lnurl_hash(secret: str):
@@ -34,8 +35,8 @@ def generate_bleskomat_lnurl_secret(api_key_id: str, signature: str):
return m.hexdigest()
def get_callback_url():
return url_for("bleskomat.api_bleskomat_lnurl", _external=True)
def get_callback_url(req: Request):
return req.url_for("bleskomat.api_bleskomat_lnurl")
def is_supported_lnurl_subprotocol(tag: str) -> bool:
+15 -21
View File
@@ -1,8 +1,9 @@
import json
import math
from quart import jsonify, request
from http import HTTPStatus
import traceback
from http import HTTPStatus
from starlette.requests import Request
from . import bleskomat_ext
from .crud import (
@@ -10,16 +11,12 @@ from .crud import (
get_bleskomat_by_api_key_id,
get_bleskomat_lnurl,
)
from .exchange_rates import (
fetch_fiat_exchange_rate,
)
from .exchange_rates import fetch_fiat_exchange_rate
from .helpers import (
generate_bleskomat_lnurl_signature,
generate_bleskomat_lnurl_secret,
LnurlHttpError,
LnurlValidationError,
generate_bleskomat_lnurl_secret,
generate_bleskomat_lnurl_signature,
prepare_lnurl_params,
query_to_signing_payload,
unshorten_lnurl_query,
@@ -27,10 +24,10 @@ from .helpers import (
# Handles signed URL from Bleskomat ATMs and "action" callback of auto-generated LNURLs.
@bleskomat_ext.route("/u", methods=["GET"])
async def api_bleskomat_lnurl():
@bleskomat_ext.get("/u", name="bleskomat.api_bleskomat_lnurl")
async def api_bleskomat_lnurl(req: Request):
try:
query = request.args.to_dict()
query = req.query_params
# Unshorten query if "s" is used instead of "signature".
if "s" in query:
@@ -99,7 +96,7 @@ async def api_bleskomat_lnurl():
)
# Reply with LNURL response object.
return jsonify(lnurl.get_info_response_object(secret)), HTTPStatus.OK
return lnurl.get_info_response_object(secret, req)
# No signature provided.
# Treat as "action" callback.
@@ -123,12 +120,9 @@ async def api_bleskomat_lnurl():
raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST)
except LnurlHttpError as e:
return jsonify({"status": "ERROR", "reason": str(e)}), e.http_status
except Exception:
traceback.print_exc()
return (
jsonify({"status": "ERROR", "reason": "Unexpected error"}),
HTTPStatus.INTERNAL_SERVER_ERROR,
)
return {"status": "ERROR", "reason": str(e)}
except Exception as e:
print(str(e))
return {"status": "ERROR", "reason": "Unexpected error"}
return jsonify({"status": "OK"}), HTTPStatus.OK
return {"status": "OK"}
+2 -2
View File
@@ -2,7 +2,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS bleskomats (
CREATE TABLE bleskomat.bleskomats (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
api_key_id TEXT NOT NULL,
@@ -19,7 +19,7 @@ async def m001_initial(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS bleskomat_lnurls (
CREATE TABLE bleskomat.bleskomat_lnurls (
id TEXT PRIMARY KEY,
bleskomat TEXT NOT NULL,
wallet TEXT NOT NULL,
+47 -15
View File
@@ -1,13 +1,45 @@
import json
import time
from typing import NamedTuple, Dict
from typing import Dict
from fastapi.params import Query
from pydantic import BaseModel, validator
from starlette.requests import Request
from lnbits import bolt11
from lnbits.core.services import pay_invoice
from lnbits.core.services import pay_invoice, PaymentFailure
from . import db
from .helpers import get_callback_url, LnurlValidationError
from .exchange_rates import exchange_rate_providers, fiat_currencies
from .helpers import LnurlValidationError, get_callback_url
class Bleskomat(NamedTuple):
class CreateBleskomat(BaseModel):
name: str = Query(...)
fiat_currency: str = Query(...)
exchange_rate_provider: str = Query(...)
fee: str = Query(...)
@validator("fiat_currency")
def allowed_fiat_currencies(cls, v):
if v not in fiat_currencies.keys():
raise ValueError("Not allowed currency")
return v
@validator("exchange_rate_provider")
def allowed_providers(cls, v):
if v not in exchange_rate_providers.keys():
raise ValueError("Not allowed provider")
return v
@validator("fee")
def fee_type(cls, v):
if not isinstance(v, (str, float, int)):
raise ValueError("Fee type not allowed")
return v
class Bleskomat(BaseModel):
id: str
wallet: str
api_key_id: str
@@ -19,7 +51,7 @@ class Bleskomat(NamedTuple):
fee: str
class BleskomatLnurl(NamedTuple):
class BleskomatLnurl(BaseModel):
id: str
bleskomat: str
wallet: str
@@ -36,14 +68,14 @@ class BleskomatLnurl(NamedTuple):
# When initial uses is 0 then the LNURL has unlimited uses.
return self.initial_uses == 0 or self.remaining_uses > 0
def get_info_response_object(self, secret: str) -> Dict[str, str]:
def get_info_response_object(self, secret: str, req: Request) -> Dict[str, str]:
tag = self.tag
params = json.loads(self.params)
response = {"tag": tag}
if tag == "withdrawRequest":
for key in ["minWithdrawable", "maxWithdrawable", "defaultDescription"]:
response[key] = params[key]
response["callback"] = get_callback_url()
response["callback"] = get_callback_url(req)
response["k1"] = secret
return response
@@ -87,20 +119,20 @@ class BleskomatLnurl(NamedTuple):
tag = self.tag
if tag == "withdrawRequest":
try:
payment_hash = await pay_invoice(
wallet_id=self.wallet,
payment_request=query["pr"],
await pay_invoice(
wallet_id=self.wallet, payment_request=query["pr"]
)
except Exception:
raise LnurlValidationError("Failed to pay invoice")
if not payment_hash:
raise LnurlValidationError("Failed to pay invoice")
except (ValueError, PermissionError, PaymentFailure) as e:
raise LnurlValidationError("Failed to pay invoice: " + str(e))
except Exception as e:
print(str(e))
raise LnurlValidationError("Unexpected error")
async def use(self, conn) -> bool:
now = int(time.time())
result = await conn.execute(
"""
UPDATE bleskomat_lnurls
UPDATE bleskomat.bleskomat_lnurls
SET remaining_uses = remaining_uses - 1, updated_time = ?
WHERE id = ?
AND remaining_uses > 0
@@ -84,7 +84,7 @@ new Vue({
LNbits.api
.request(
'GET',
'/bleskomat/api/v1/bleskomats?all_wallets',
'/bleskomat/api/v1/bleskomats?all_wallets=true',
this.g.user.wallets[0].adminkey
)
.then(function (response) {
@@ -1,7 +1,7 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Bleskomat Extension for lnbits"
label="Setup guide"
:content-inset-level="0.5"
>
<q-card>
@@ -11,7 +11,7 @@
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
<q-btn unelevated color="primary" @click="formDialog.show = true"
>Add Bleskomat</q-btn
>
</q-card-section>
@@ -94,7 +94,9 @@
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Bleskomat extension</h6>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} Bleskomat extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
@@ -150,14 +152,14 @@
<q-btn
v-if="formDialog.data.id"
unelevated
color="deep-purple"
color="primary"
type="submit"
>Update Bleskomat</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
color="primary"
:disable="
formDialog.data.wallet == null ||
formDialog.data.name == null ||
+15 -11
View File
@@ -1,22 +1,26 @@
from quart import g, render_template
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.decorators import check_user_exists, validate_uuids
from . import bleskomat_ext
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import bleskomat_ext, bleskomat_renderer
from .exchange_rates import exchange_rate_providers_serializable, fiat_currencies
from .helpers import get_callback_url
templates = Jinja2Templates(directory="templates")
@bleskomat_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
@bleskomat_ext.get("/", response_class=HTMLResponse)
async def index(req: Request, user: User = Depends(check_user_exists)):
bleskomat_vars = {
"callback_url": get_callback_url(),
"callback_url": get_callback_url(req),
"exchange_rate_providers": exchange_rate_providers_serializable,
"fiat_currencies": fiat_currencies,
}
return await render_template(
"bleskomat/index.html", user=g.user, bleskomat_vars=bleskomat_vars
return bleskomat_renderer().TemplateResponse(
"bleskomat/index.html",
{"request": req, "user": user.dict(), "bleskomat_vars": bleskomat_vars},
)
+54 -77
View File
@@ -1,120 +1,97 @@
from quart import g, jsonify, request
from http import HTTPStatus
from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.decorators import WalletTypeInfo, require_admin_key
from lnbits.extensions.bleskomat.models import CreateBleskomat
from . import bleskomat_ext
from .crud import (
create_bleskomat,
delete_bleskomat,
get_bleskomat,
get_bleskomats,
update_bleskomat,
delete_bleskomat,
)
from .exchange_rates import (
exchange_rate_providers,
fetch_fiat_exchange_rate,
fiat_currencies,
)
from .exchange_rates import fetch_fiat_exchange_rate
@bleskomat_ext.route("/api/v1/bleskomats", methods=["GET"])
@api_check_wallet_key("admin")
async def api_bleskomats():
wallet_ids = [g.wallet.id]
@bleskomat_ext.get("/api/v1/bleskomats")
async def api_bleskomats(
wallet: WalletTypeInfo = Depends(require_admin_key),
all_wallets: bool = Query(False),
):
wallet_ids = [wallet.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
return (
jsonify(
[bleskomat._asdict() for bleskomat in await get_bleskomats(wallet_ids)]
),
HTTPStatus.OK,
)
return [bleskomat.dict() for bleskomat in await get_bleskomats(wallet_ids)]
@bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["GET"])
@api_check_wallet_key("admin")
async def api_bleskomat_retrieve(bleskomat_id):
@bleskomat_ext.get("/api/v1/bleskomat/{bleskomat_id}")
async def api_bleskomat_retrieve(
bleskomat_id, wallet: WalletTypeInfo = Depends(require_admin_key)
):
bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id:
return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
if not bleskomat or bleskomat.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Bleskomat configuration not found.",
)
return jsonify(bleskomat._asdict()), HTTPStatus.OK
return bleskomat.dict()
@bleskomat_ext.route("/api/v1/bleskomat", methods=["POST"])
@bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["PUT"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"name": {"type": "string", "empty": False, "required": True},
"fiat_currency": {
"type": "string",
"allowed": fiat_currencies.keys(),
"required": True,
},
"exchange_rate_provider": {
"type": "string",
"allowed": exchange_rate_providers.keys(),
"required": True,
},
"fee": {"type": ["string", "float", "number", "integer"], "required": True},
}
)
async def api_bleskomat_create_or_update(bleskomat_id=None):
@bleskomat_ext.post("/api/v1/bleskomat")
@bleskomat_ext.put("/api/v1/bleskomat/{bleskomat_id}")
async def api_bleskomat_create_or_update(
data: CreateBleskomat,
wallet: WalletTypeInfo = Depends(require_admin_key),
bleskomat_id=None,
):
try:
fiat_currency = g.data["fiat_currency"]
exchange_rate_provider = g.data["exchange_rate_provider"]
fiat_currency = data.fiat_currency
exchange_rate_provider = data.exchange_rate_provider
await fetch_fiat_exchange_rate(
currency=fiat_currency, provider=exchange_rate_provider
)
except Exception as e:
print(e)
return (
jsonify(
{
"message": f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"'
}
),
HTTPStatus.INTERNAL_SERVER_ERROR,
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"',
)
if bleskomat_id:
bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id:
return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
if not bleskomat or bleskomat.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Bleskomat configuration not found.",
)
bleskomat = await update_bleskomat(bleskomat_id, **g.data)
bleskomat = await update_bleskomat(bleskomat_id, **data.dict())
else:
bleskomat = await create_bleskomat(wallet_id=g.wallet.id, **g.data)
bleskomat = await create_bleskomat(wallet_id=wallet.wallet.id, data=data)
return (
jsonify(bleskomat._asdict()),
HTTPStatus.OK if bleskomat_id else HTTPStatus.CREATED,
)
return bleskomat.dict()
@bleskomat_ext.route("/api/v1/bleskomat/<bleskomat_id>", methods=["DELETE"])
@api_check_wallet_key("admin")
async def api_bleskomat_delete(bleskomat_id):
@bleskomat_ext.delete("/api/v1/bleskomat/{bleskomat_id}")
async def api_bleskomat_delete(
bleskomat_id, wallet: WalletTypeInfo = Depends(require_admin_key)
):
bleskomat = await get_bleskomat(bleskomat_id)
if not bleskomat or bleskomat.wallet != g.wallet.id:
return (
jsonify({"message": "Bleskomat configuration not found."}),
HTTPStatus.NOT_FOUND,
if not bleskomat or bleskomat.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Bleskomat configuration not found.",
)
await delete_bleskomat(bleskomat_id)
return "", HTTPStatus.NO_CONTENT
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
-11
View File
@@ -1,11 +0,0 @@
<h1>Example Extension</h1>
<h2>*tagline*</h2>
This is an example extension to help you organise and build you own.
Try to include an image
<img src="https://i.imgur.com/9i4xcQB.png">
<h2>If your extension has API endpoints, include useful ones here</h2>
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
-12
View File
@@ -1,12 +0,0 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_captcha")
captcha_ext: Blueprint = Blueprint(
"captcha", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Captcha",
"short_description": "Create captcha to stop spam",
"icon": "block",
"contributors": ["pseudozach"]
}
-51
View File
@@ -1,51 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Captcha
async def create_captcha(
*,
wallet_id: str,
url: str,
memo: str,
description: Optional[str] = None,
amount: int = 0,
remembers: bool = True,
) -> Captcha:
captcha_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO captchas (id, wallet, url, memo, description, amount, remembers)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(captcha_id, wallet_id, url, memo, description, amount, int(remembers)),
)
captcha = await get_captcha(captcha_id)
assert captcha, "Newly created captcha couldn't be retrieved"
return captcha
async def get_captcha(captcha_id: str) -> Optional[Captcha]:
row = await db.fetchone("SELECT * FROM captchas WHERE id = ?", (captcha_id,))
return Captcha.from_row(row) if row else None
async def get_captchas(wallet_ids: Union[str, List[str]]) -> List[Captcha]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM captchas WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Captcha.from_row(row) for row in rows]
async def delete_captcha(captcha_id: str) -> None:
await db.execute("DELETE FROM captchas WHERE id = ?", (captcha_id,))
-67
View File
@@ -1,67 +0,0 @@
from sqlalchemy.exc import OperationalError # type: ignore
async def m001_initial(db):
"""
Initial captchas table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS captchas (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
secret TEXT NOT NULL,
url TEXT NOT NULL,
memo TEXT NOT NULL,
amount INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)
async def m002_redux(db):
"""
Creates an improved captchas table and migrates the existing data.
"""
try:
await db.execute("SELECT remembers FROM captchas")
except OperationalError:
await db.execute("ALTER TABLE captchas RENAME TO captchas_old")
await db.execute(
"""
CREATE TABLE IF NOT EXISTS captchas (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
url TEXT NOT NULL,
memo TEXT NOT NULL,
description TEXT NULL,
amount INTEGER DEFAULT 0,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')),
remembers INTEGER DEFAULT 0,
extras TEXT NULL
);
"""
)
await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON captchas (wallet)")
for row in [
list(row) for row in await db.fetchall("SELECT * FROM captchas_old")
]:
await db.execute(
"""
INSERT INTO captchas (
id,
wallet,
url,
memo,
amount,
time
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(row[0], row[1], row[3], row[4], row[5], row[6]),
)
await db.execute("DROP TABLE captchas_old")
-23
View File
@@ -1,23 +0,0 @@
import json
from sqlite3 import Row
from typing import NamedTuple, Optional
class Captcha(NamedTuple):
id: str
wallet: str
url: str
memo: str
description: str
amount: int
time: int
remembers: bool
extras: Optional[dict]
@classmethod
def from_row(cls, row: Row) -> "Captcha":
data = dict(row)
data["remembers"] = bool(data["remembers"])
data["extras"] = json.loads(data["extras"]) if data["extras"] else None
return cls(**data)
@@ -1,82 +0,0 @@
var ciframeLoaded = !1,
captchaStyleAdded = !1
function ccreateIframeElement(t = {}) {
const e = document.createElement('iframe')
// e.style.marginLeft = "25px",
;(e.style.border = 'none'),
(e.style.width = '100%'),
(e.style.height = '100%'),
(e.scrolling = 'no'),
(e.id = 'captcha-iframe')
t.dest, t.amount, t.currency, t.label, t.opReturn
var captchaid = document
.getElementById('captchascript')
.getAttribute('data-captchaid')
var lnbhostsrc = document.getElementById('captchascript').getAttribute('src')
var lnbhost = lnbhostsrc.split('/captcha/static/js/captcha.js')[0]
return (e.src = lnbhost + '/captcha/' + captchaid), e
}
document.addEventListener('DOMContentLoaded', function () {
if (captchaStyleAdded) console.log('Captcha already added!')
else {
console.log('Adding captcha'), (captchaStyleAdded = !0)
var t = document.createElement('style')
t.innerHTML =
"\t/*Button*/\t\t.button-captcha-filled\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #FF7979;\t\t\tbox-shadow: 0 2px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\tbox-shadow: 0 0 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.button-captcha-filled-dark\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled-dark:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled-dark:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.modal-captcha-container {\t\t position: fixed;\t\t z-index: 1000;\t\t text-align: left;/*Si no añado esto, a veces hereda el text-align:center del body, y entonces el popup queda movido a la derecha, por center + margin left que aplico*/\t\t left: 0;\t\t top: 0;\t\t width: 100%;\t\t height: 100%;\t\t background-color: rgba(0, 0, 0, 0.5);\t\t opacity: 0;\t\t visibility: hidden;\t\t transform: scale(1.1);\t\t transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t.modal-captcha-content {\t\t position: absolute;\t\t top: 50%;\t\t left: 50%;\t\t transform: translate(-50%, -50%);\t\t background-color: white;\t\t width: 100%;\t\t height: 100%;\t\t border-radius: 0.5rem;\t\t /*Rounded shadowed borders*/\t\t\tbox-shadow: 2px 2px 4px 0 rgba(0,0,0,0.15);\t\t\tborder-radius: 5px;\t\t}\t\t.close-button-captcha {\t\t float: right;\t\t width: 1.5rem;\t\t line-height: 1.5rem;\t\t text-align: center;\t\t cursor: pointer;\t\t margin-right:20px;\t\t margin-top:10px;\t\t border-radius: 0.25rem;\t\t background-color: lightgray;\t\t}\t\t.close-button-captcha:hover {\t\t background-color: darkgray;\t\t}\t\t.show-modal-captcha {\t\t opacity: 1;\t\t visibility: visible;\t\t transform: scale(1.0);\t\t transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t/* Mobile */\t\t@media screen and (min-device-width: 160px) and ( max-width: 1077px ) /*No tendria ni por que poner un min-device, porq abarca todo lo humano...*/\t\t{\t\t}"
var e = document.querySelector('script')
e.parentNode.insertBefore(t, e)
var i = document.getElementById('captchacheckbox'),
n = i.dataset,
o = 'true' === n.dark
var a = document.createElement('div')
;(a.className += ' modal-captcha-container'),
(a.innerHTML =
'\t\t<div class="modal-captcha-content"> \t<span class="close-button-captcha" style="display: none;">&times;</span>\t\t</div>\t'),
document.getElementsByTagName('body')[0].appendChild(a)
var r = document.getElementsByClassName('modal-captcha-content').item(0)
document
.getElementsByClassName('close-button-captcha')
.item(0)
.addEventListener('click', d),
window.addEventListener('click', function (t) {
t.target === a && d()
}),
i.addEventListener('change', function () {
if (this.checked) {
// console.log("checkbox checked");
if (0 == ciframeLoaded) {
// console.log("n: ", n);
var t = ccreateIframeElement(n)
r.appendChild(t), (ciframeLoaded = !0)
}
d()
}
})
}
function d() {
a.classList.toggle('show-modal-captcha')
}
})
function receiveMessage(event) {
if (event.data.includes('paymenthash')) {
// console.log("paymenthash received: ", event.data);
document.getElementById('captchapayhash').value = event.data.split('_')[1]
}
if (event.data.includes('removetheiframe')) {
if (event.data.includes('nok')) {
//invoice was NOT paid
// console.log("receiveMessage not paid")
document.getElementById('captchacheckbox').checked = false
}
ciframeLoaded = !1
var element = document.getElementById('captcha-iframe')
document
.getElementsByClassName('modal-captcha-container')[0]
.classList.toggle('show-modal-captcha')
element.parentNode.removeChild(element)
}
}
window.addEventListener('message', receiveMessage, false)
@@ -1,147 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List captchas">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /captcha/api/v1/captchas</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;captcha_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}captcha/api/v1/captchas -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a captcha">
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span> /captcha/api/v1/captchas</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"amount": &lt;integer&gt;, "description": &lt;string&gt;, "memo":
&lt;string&gt;, "remembers": &lt;boolean&gt;, "url":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"amount": &lt;integer&gt;, "description": &lt;string&gt;, "id":
&lt;string&gt;, "memo": &lt;string&gt;, "remembers": &lt;boolean&gt;,
"time": &lt;int&gt;, "url": &lt;string&gt;, "wallet":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}captcha/api/v1/captchas -d
'{"url": &lt;string&gt;, "memo": &lt;string&gt;, "description":
&lt;string&gt;, "amount": &lt;integer&gt;, "remembers":
&lt;boolean&gt;}' -H "Content-type: application/json" -H "X-Api-Key:
{{ g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create an invoice (public)"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/captcha/api/v1/captchas/&lt;captcha_id&gt;/invoice</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"amount": &lt;integer&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"payment_hash": &lt;string&gt;, "payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}captcha/api/v1/captchas/&lt;captcha_id&gt;/invoice -d '{"amount":
&lt;integer&gt;}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check invoice status (public)"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/captcha/api/v1/captchas/&lt;captcha_id&gt;/check_invoice</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"payment_hash": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"paid": false}</code><br />
<code
>{"paid": true, "url": &lt;string&gt;, "remembers":
&lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}captcha/api/v1/captchas/&lt;captcha_id&gt;/check_invoice -d
'{"payment_hash": &lt;string&gt;}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a captcha"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/captcha/api/v1/captchas/&lt;captcha_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 />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root
}}captcha/api/v1/captchas/&lt;captcha_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -1,178 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-8 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<h5 class="text-subtitle1 q-mt-none q-mb-sm">{{ captcha.memo }}</h5>
{% if captcha.description %}
<p>{{ captcha.description }}</p>
{% endif %}
<div v-if="!this.redirectUrl" class="q-mt-lg">
<q-form v-if="">
<q-input
filled
v-model.number="userAmount"
type="number"
:min="captchaAmount"
suffix="sat"
label="Choose an amount *"
:hint="'Minimum ' + captchaAmount + ' sat'"
>
<template v-slot:after>
<q-btn
round
dense
flat
icon="check"
color="deep-purple"
type="submit"
@click="createInvoice"
:disabled="userAmount < captchaAmount || paymentReq"
></q-btn>
</template>
</q-input>
</q-form>
<div v-if="paymentReq" class="q-mt-lg">
<a :href="'lightning:' + paymentReq">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="paymentReq"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(paymentReq)"
>Copy invoice</q-btn
>
<q-btn
@click="cancelPayment(false)"
flat
color="grey"
class="q-ml-auto"
>Cancel</q-btn
>
</div>
</div>
</div>
<div v-else>
<q-separator class="q-my-lg"></q-separator>
<p>
Captcha accepted. You are probably human.<br />
<!-- <strong>{% raw %}{{ redirectUrl }}{% endraw %}</strong> -->
</p>
<!-- <div class="row q-mt-lg">
<q-btn outline color="grey" type="a" :href="redirectUrl"
>Open URL</q-btn>
</div> -->
</div>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
userAmount: {{ captcha.amount }},
captchaAmount: {{ captcha.amount }},
paymentReq: null,
redirectUrl: null,
paymentDialog: {
dismissMsg: null,
checker: null
}
}
},
computed: {
amount: function () {
return (this.captchaAmount > this.userAmount) ? this.captchaAmount : this.userAmount
}
},
methods: {
cancelPayment: function (paid) {
this.paymentReq = null
clearInterval(this.paymentDialog.checker)
if (this.paymentDialog.dismissMsg) {
this.paymentDialog.dismissMsg()
}
var removeiframestring = "removetheiframe_nok";
var timeout = 500;
if(paid){
console.log("paid, dismissing iframe");
removeiframestring = "removetheiframe_ok";
timeout = 2000;
}
setTimeout(function () {
// parent.closeIFrame()
parent.window.postMessage(removeiframestring, "*");
}, timeout)
},
createInvoice: function () {
var self = this
axios
.post(
'/captcha/api/v1/captchas/{{ captcha.id }}/invoice',
{amount: this.amount}
)
.then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.checker = setInterval(function () {
axios
.post(
'/captcha/api/v1/captchas/{{ captcha.id }}/check_invoice',
{payment_hash: response.data.payment_hash}
)
.then(function (res) {
if (res.data.paid) {
self.cancelPayment(true)
self.redirectUrl = res.data.url
if (res.data.remembers) {
self.$q.localStorage.set(
'lnbits.captcha.{{ captcha.id }}',
res.data.url
)
}
parent.window.postMessage("paymenthash_"+response.data.payment_hash, "*");
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)
})
}
},
created: function () {
var url = this.$q.localStorage.getItem('lnbits.captcha.{{ captcha.id }}')
if (url) {
this.redirectUrl = url
}
}
})
</script>
{% endblock %}
@@ -1,425 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
>New captcha</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Captchas</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="captchas"
row-key="id"
:columns="captchasTable.columns"
:pagination.sync="captchasTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.displayUrl"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
:href="buildCaptchaSnippet(props.row.id)"
@click="openQrCodeDialog(props.row.id)"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteCaptcha(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits captcha extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "captcha/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="createCaptcha" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<!-- <q-input
filled
dense
v-model.trim="formDialog.data.url"
type="hidden"
label="Redirect URL *"
:value="https://dummy.com"
></q-input> -->
<q-input
filled
dense
v-model.trim="formDialog.data.memo"
label="Title *"
placeholder="LNbits captcha"
></q-input>
<q-input
filled
dense
autogrow
v-model.trim="formDialog.data.description"
label="Description"
></q-input>
<q-input
filled
dense
v-model.number="formDialog.data.amount"
type="number"
label="Amount (sat) *"
hint="This is the minimum amount users can pay/donate."
></q-input>
<q-list>
<q-item tag="label" class="rounded-borders">
<q-item-section avatar>
<q-checkbox
v-model="formDialog.data.remembers"
color="deep-purple"
></q-checkbox>
</q-item-section>
<q-item-section>
<q-item-label>Remember payments</q-item-label>
<q-item-label caption
>A succesful payment will be registered in the browser's
storage, so the user doesn't need to pay again to prove they are
human.</q-item-label
>
</q-item-section>
</q-item>
</q-list>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="formDialog.data.amount == null || formDialog.data.amount < 0 || formDialog.data.memo == null"
type="submit"
>Create captcha</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
{% raw %}
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<!-- <qrcode
:value="qrCodeDialog.data.lnurl"
:options="{width: 800}"
class="rounded-borders"
></qrcode> -->
<code style="word-break: break-all">
{{ qrCodeDialog.data.snippet }}
</code>
<p style="margin-top: 20px">
Copy the snippet above and paste into your website/form. The checkbox
can be in checked state only after user pays.
</p>
</q-responsive>
<p style="word-break: break-all">
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br />
<!-- <span v-if="qrCodeDialog.data.currency"
><strong>{{ qrCodeDialog.data.currency }} price:</strong> {{
fiatRates[qrCodeDialog.data.currency] ?
fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}<br
/></span>
<strong>Accepts comments:</strong> {{ qrCodeDialog.data.comments }}<br />
<strong>Dispatches webhook to:</strong> {{ qrCodeDialog.data.webhook
}}<br />
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br /> -->
</p>
{% endraw %}
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.snippet, 'Snippet copied to clipboard!')"
class="q-ml-sm"
>Copy Snippet</q-btn
>
<!-- <q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
>Shareable link</q-btn
>
<q-btn
outline
color="grey"
icon="print"
type="a"
:href="qrCodeDialog.data.print_url"
target="_blank"
></q-btn> -->
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapCaptcha = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.displayUrl = ['/captcha/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
captchas: [],
captchasTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'memo', align: 'left', label: 'Memo', field: 'memo'},
{
name: 'amount',
align: 'right',
label: 'Amount (sat)',
field: 'fsat',
sortable: true,
sort: function (a, b, rowA, rowB) {
return rowA.amount - rowB.amount
}
},
{
name: 'remembers',
align: 'left',
label: 'Remember',
field: 'remembers'
},
{
name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {
remembers: false
}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
methods: {
getCaptchas: function () {
var self = this
LNbits.api
.request(
'GET',
'/captcha/api/v1/captchas?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.captchas = response.data.map(function (obj) {
return mapCaptcha(obj)
})
})
},
createCaptcha: function () {
var data = {
// url: this.formDialog.data.url,
url: 'http://dummy.com',
memo: this.formDialog.data.memo,
amount: this.formDialog.data.amount,
description: this.formDialog.data.description,
remembers: this.formDialog.data.remembers
}
var self = this
LNbits.api
.request(
'POST',
'/captcha/api/v1/captchas',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.captchas.push(mapCaptcha(response.data))
self.formDialog.show = false
self.formDialog.data = {
remembers: false
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteCaptcha: function (captchaId) {
var self = this
var captcha = _.findWhere(this.captchas, {id: captchaId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this captcha link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/captcha/api/v1/captchas/' + captchaId,
_.findWhere(self.g.user.wallets, {id: captcha.wallet}).inkey
)
.then(function (response) {
self.captchas = _.reject(self.captchas, function (obj) {
return obj.id == captchaId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
buildCaptchaSnippet: function (captchaId) {
var locationPath = [
window.location.protocol,
'//',
window.location.host,
window.location.pathname
].join('')
var captchasnippet =
'<!-- Captcha Checkbox Start -->\n' +
'<input type="checkbox" id="captchacheckbox">\n' +
'<label for="captchacheckbox">I\'m not a robot</label><br/>\n' +
'<input type="text" id="captchapayhash" style="display: none;"/>\n' +
'<script type="text/javascript" src="' +
locationPath +
'static/js/captcha.js" id="captchascript" data-captchaid="' +
captchaId +
'">\n' +
'<\/script>\n' +
'<!-- Captcha Checkbox End -->'
return captchasnippet
},
openQrCodeDialog(captchaId) {
// var link = _.findWhere(this.payLinks, {id: linkId})
var captcha = _.findWhere(this.captchas, {id: captchaId})
// if (link.currency) this.updateFiatRate(link.currency)
this.qrCodeDialog.data = {
id: captcha.id,
amount: captcha.amount,
// (link.min === link.max ? link.min : `${link.min} - ${link.max}`) +
// ' ' +
// (link.currency || 'sat'),
snippet: this.buildCaptchaSnippet(captcha.id)
// currency: link.currency,
// comments: link.comment_chars
// ? `${link.comment_chars} characters`
// : 'no',
// webhook: link.webhook_url || 'nowhere',
// success:
// link.success_text || link.success_url
// ? 'Display message "' +
// link.success_text +
// '"' +
// (link.success_url ? ' and URL "' + link.success_url + '"' : '')
// : 'do nothing',
// lnurl: link.lnurl,
// pay_url: link.pay_url,
// print_url: link.print_url
}
this.qrCodeDialog.show = true
},
exportCSV: function () {
LNbits.utils.exportCSV(this.captchasTable.columns, this.captchas)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getCaptchas()
}
}
})
</script>
{% endblock %}
-22
View File
@@ -1,22 +0,0 @@
from quart import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from . import captcha_ext
from .crud import get_captcha
@captcha_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("captcha/index.html", user=g.user)
@captcha_ext.route("/<captcha_id>")
async def display(captcha_id):
captcha = await get_captcha(captcha_id) or abort(
HTTPStatus.NOT_FOUND, "captcha does not exist."
)
return await render_template("captcha/display.html", captcha=captcha)
-121
View File
@@ -1,121 +0,0 @@
from quart import g, jsonify, request
from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from . import captcha_ext
from .crud import create_captcha, get_captcha, get_captchas, delete_captcha
@captcha_ext.route("/api/v1/captchas", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_captchas():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return (
jsonify([captcha._asdict() for captcha in await get_captchas(wallet_ids)]),
HTTPStatus.OK,
)
@captcha_ext.route("/api/v1/captchas", methods=["POST"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"url": {"type": "string", "empty": False, "required": True},
"memo": {"type": "string", "empty": False, "required": True},
"description": {
"type": "string",
"empty": True,
"nullable": True,
"required": False,
},
"amount": {"type": "integer", "min": 0, "required": True},
"remembers": {"type": "boolean", "required": True},
}
)
async def api_captcha_create():
captcha = await create_captcha(wallet_id=g.wallet.id, **g.data)
return jsonify(captcha._asdict()), HTTPStatus.CREATED
@captcha_ext.route("/api/v1/captchas/<captcha_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
async def api_captcha_delete(captcha_id):
captcha = await get_captcha(captcha_id)
if not captcha:
return jsonify({"message": "captcha does not exist."}), HTTPStatus.NOT_FOUND
if captcha.wallet != g.wallet.id:
return jsonify({"message": "Not your captcha."}), HTTPStatus.FORBIDDEN
await delete_captcha(captcha_id)
return "", HTTPStatus.NO_CONTENT
@captcha_ext.route("/api/v1/captchas/<captcha_id>/invoice", methods=["POST"])
@api_validate_post_request(
schema={"amount": {"type": "integer", "min": 1, "required": True}}
)
async def api_captcha_create_invoice(captcha_id):
captcha = await get_captcha(captcha_id)
if g.data["amount"] < captcha.amount:
return (
jsonify({"message": f"Minimum amount is {captcha.amount} sat."}),
HTTPStatus.BAD_REQUEST,
)
try:
amount = (
g.data["amount"] if g.data["amount"] > captcha.amount else captcha.amount
)
payment_hash, payment_request = await create_invoice(
wallet_id=captcha.wallet,
amount=amount,
memo=f"{captcha.memo}",
extra={"tag": "captcha"},
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.CREATED,
)
@captcha_ext.route("/api/v1/captchas/<captcha_id>/check_invoice", methods=["POST"])
@api_validate_post_request(
schema={"payment_hash": {"type": "string", "empty": False, "required": True}}
)
async def api_paywal_check_invoice(captcha_id):
captcha = await get_captcha(captcha_id)
if not captcha:
return jsonify({"message": "captcha does not exist."}), HTTPStatus.NOT_FOUND
try:
status = await check_invoice_status(captcha.wallet, g.data["payment_hash"])
is_paid = not status.pending
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
if is_paid:
wallet = await get_wallet(captcha.wallet)
payment = await wallet.get_payment(g.data["payment_hash"])
await payment.set_pending(False)
return (
jsonify({"paid": True, "url": captcha.url, "remembers": captcha.remembers}),
HTTPStatus.OK,
)
return jsonify({"paid": False}), HTTPStatus.OK
+3
View File
@@ -0,0 +1,3 @@
# StreamerCopilot
Tool to help streamers accept sats for tips
+34
View File
@@ -0,0 +1,34 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_copilot")
copilot_static_files = [
{
"path": "/copilot/static",
"app": StaticFiles(directory="lnbits/extensions/copilot/static"),
"name": "copilot_static",
}
]
copilot_ext: APIRouter = APIRouter(prefix="/copilot", tags=["copilot"])
def copilot_renderer():
return template_renderer(["lnbits/extensions/copilot/templates"])
from .lnurl import * # noqa
from .tasks import wait_for_paid_invoices
from .views import * # noqa
from .views_api import * # noqa
def copilot_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+8
View File
@@ -0,0 +1,8 @@
{
"name": "Streamer Copilot",
"short_description": "Video tips/animations/webhooks",
"icon": "face",
"contributors": [
"arcbtc"
]
}
+96
View File
@@ -0,0 +1,96 @@
from typing import List, Optional, Union
from . import db
from .models import Copilots, CreateCopilotData
from lnbits.helpers import urlsafe_short_hash
###############COPILOTS##########################
async def create_copilot(
data: CreateCopilotData, inkey: Optional[str] = ""
) -> Copilots:
copilot_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO copilot.newer_copilots (
id,
user,
lnurl_toggle,
wallet,
title,
animation1,
animation2,
animation3,
animation1threshold,
animation2threshold,
animation3threshold,
animation1webhook,
animation2webhook,
animation3webhook,
lnurl_title,
show_message,
show_ack,
show_price,
fullscreen_cam,
iframe_url,
amount_made
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
copilot_id,
data.user,
int(data.lnurl_toggle),
data.wallet,
data.title,
data.animation1,
data.animation2,
data.animation3,
data.animation1threshold,
data.animation2threshold,
data.animation3threshold,
data.animation1webhook,
data.animation2webhook,
data.animation3webhook,
data.lnurl_title,
int(data.show_message),
int(data.show_ack),
data.show_price,
0,
None,
0,
),
)
return await get_copilot(copilot_id)
async def update_copilot(
data: CreateCopilotData, copilot_id: Optional[str] = ""
) -> Optional[Copilots]:
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
items.append(copilot_id)
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
row = await db.fetchone(
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
)
return Copilots(**row) if row else None
async def get_copilot(copilot_id: str) -> Copilots:
row = await db.fetchone(
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
)
return Copilots(**row) if row else None
async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall(
"SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,)
)
return [Copilots(**row) for row in rows]
async def delete_copilot(copilot_id: str) -> None:
await db.execute("DELETE FROM copilot.newer_copilots WHERE id = ?", (copilot_id,))
+84
View File
@@ -0,0 +1,84 @@
import hashlib
import json
from http import HTTPStatus
from fastapi import Request
from fastapi.param_functions import Query
from lnurl.types import LnurlPayMetadata
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse # type: ignore
from lnbits.core.services import create_invoice
from . import copilot_ext
from .crud import get_copilot
@copilot_ext.get(
"/lnurl/{cp_id}", response_class=HTMLResponse, name="copilot.lnurl_response"
)
async def lnurl_response(req: Request, cp_id: str = Query(None)):
cp = await get_copilot(cp_id)
if not cp:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
)
payResponse = {
"tag": "payRequest",
"callback": req.url_for("copilot.lnurl_callback", cp_id=cp_id),
"metadata": LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])),
"maxSendable": 50000000,
"minSendable": 10000,
}
if cp.show_message:
payResponse["commentAllowed"] = 300
return json.dumps(payResponse)
@copilot_ext.get(
"/lnurl/cb/{cp_id}", response_class=HTMLResponse, name="copilot.lnurl_callback"
)
async def lnurl_callback(
cp_id: str = Query(None), amount: str = Query(None), comment: str = Query(None)
):
cp = await get_copilot(cp_id)
if not cp:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
)
amount_received = int(amount)
if amount_received < 10000:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Amount {round(amount_received / 1000)} is smaller than minimum 10 sats.",
)
elif amount_received / 1000 > 10000000:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Amount {round(amount_received / 1000)} is greater than maximum 50000.",
)
comment = ""
if comment:
if len(comment or "") > 300:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Got a comment with {len(comment)} characters, but can only accept 300",
)
if len(comment) < 1:
comment = "none"
_, payment_request = await create_invoice(
wallet_id=cp.wallet,
amount=int(amount_received / 1000),
memo=cp.lnurl_title,
description_hash=hashlib.sha256(
(
LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]]))
).encode("utf-8")
).digest(),
extra={"tag": "copilot", "copilotid": cp.id, "comment": comment},
)
payResponse = {"pr": payment_request, "routes": []}
return json.dumps(payResponse)
+79
View File
@@ -0,0 +1,79 @@
async def m001_initial(db):
"""
Initial copilot table.
"""
await db.execute(
f"""
CREATE TABLE copilot.copilots (
id TEXT NOT NULL PRIMARY KEY,
"user" TEXT,
title TEXT,
lnurl_toggle INTEGER,
wallet TEXT,
animation1 TEXT,
animation2 TEXT,
animation3 TEXT,
animation1threshold INTEGER,
animation2threshold INTEGER,
animation3threshold INTEGER,
animation1webhook TEXT,
animation2webhook TEXT,
animation3webhook TEXT,
lnurl_title TEXT,
show_message INTEGER,
show_ack INTEGER,
show_price INTEGER,
amount_made INTEGER,
fullscreen_cam INTEGER,
iframe_url TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m002_fix_data_types(db):
"""
Fix data types.
"""
if db.type != "SQLITE":
await db.execute(
"ALTER TABLE copilot.copilots ALTER COLUMN show_price TYPE TEXT;"
)
async def m003_fix_data_types(db):
await db.execute(
f"""
CREATE TABLE copilot.newer_copilots (
id TEXT NOT NULL PRIMARY KEY,
"user" TEXT,
title TEXT,
lnurl_toggle INTEGER,
wallet TEXT,
animation1 TEXT,
animation2 TEXT,
animation3 TEXT,
animation1threshold INTEGER,
animation2threshold INTEGER,
animation3threshold INTEGER,
animation1webhook TEXT,
animation2webhook TEXT,
animation3webhook TEXT,
lnurl_title TEXT,
show_message INTEGER,
show_ack INTEGER,
show_price TEXT,
amount_made INTEGER,
fullscreen_cam INTEGER,
iframe_url TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
"INSERT INTO copilot.newer_copilots SELECT * FROM copilot.copilots"
)
+64
View File
@@ -0,0 +1,64 @@
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
from starlette.requests import Request
from fastapi.param_functions import Query
from typing import Optional, Dict
from lnbits.lnurl import encode as lnurl_encode # type: ignore
from lnurl.types import LnurlPayMetadata # type: ignore
from pydantic import BaseModel
import json
from sqlite3 import Row
class CreateCopilotData(BaseModel):
user: str = Query(None)
title: str = Query(None)
lnurl_toggle: int = Query(0)
wallet: str = Query(None)
animation1: str = Query(None)
animation2: str = Query(None)
animation3: str = Query(None)
animation1threshold: int = Query(None)
animation2threshold: int = Query(None)
animation3threshold: int = Query(None)
animation1webhook: str = Query(None)
animation2webhook: str = Query(None)
animation3webhook: str = Query(None)
lnurl_title: str = Query(None)
show_message: int = Query(0)
show_ack: int = Query(0)
show_price: str = Query(None)
amount_made: int = Query(0)
timestamp: int = Query(0)
fullscreen_cam: int = Query(0)
iframe_url: str = Query(None)
success_url: str = Query(None)
class Copilots(BaseModel):
id: str
user: str = Query(None)
title: str = Query(None)
lnurl_toggle: int = Query(0)
wallet: str = Query(None)
animation1: str = Query(None)
animation2: str = Query(None)
animation3: str = Query(None)
animation1threshold: int = Query(None)
animation2threshold: int = Query(None)
animation3threshold: int = Query(None)
animation1webhook: str = Query(None)
animation2webhook: str = Query(None)
animation3webhook: str = Query(None)
lnurl_title: str = Query(None)
show_message: int = Query(0)
show_ack: int = Query(0)
show_price: str = Query(None)
amount_made: int = Query(0)
timestamp: int = Query(0)
fullscreen_cam: int = Query(0)
iframe_url: str = Query(None)
success_url: str = Query(None)
def lnurl(self, req: Request) -> str:
url = req.url_for("copilot.lnurl_response", cp_id=self.id)
return lnurl_encode(url)
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

+81
View File
@@ -0,0 +1,81 @@
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 .crud import get_copilot
from .views import updater
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:
webhook = None
data = None
if "copilot" != payment.extra.get("tag"):
# not an copilot invoice
return
copilot = await get_copilot(payment.extra.get("copilotid", -1))
if not copilot:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
)
if copilot.animation1threshold:
if int(payment.amount / 1000) >= copilot.animation1threshold:
data = copilot.animation1
webhook = copilot.animation1webhook
if copilot.animation2threshold:
if int(payment.amount / 1000) >= copilot.animation2threshold:
data = copilot.animation2
webhook = copilot.animation1webhook
if copilot.animation3threshold:
if int(payment.amount / 1000) >= copilot.animation3threshold:
data = copilot.animation3
webhook = copilot.animation1webhook
if webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(
webhook,
json={
"payment_hash": payment.payment_hash,
"payment_request": payment.bolt11,
"amount": payment.amount,
"comment": payment.extra.get("comment"),
},
timeout=40,
)
await mark_webhook_sent(payment, r.status_code)
except (httpx.ConnectError, httpx.RequestError):
await mark_webhook_sent(payment, -1)
if payment.extra.get("comment"):
await updater(copilot.id, data, payment.extra.get("comment"))
await updater(copilot.id, data, "none")
async def mark_webhook_sent(payment: Payment, status: int) -> None:
payment.extra["wh_status"] = status
await core_db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)
@@ -0,0 +1,172 @@
<q-card>
<q-card-section>
<p>
StreamerCopilot: get tips via static QR (lnurl-pay) and show an
animation<br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="Create copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">POST</span> /copilot/api/v1/copilot</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/copilot -d '{"title":
&lt;string&gt;, "animation": &lt;string&gt;,
"show_message":&lt;string&gt;, "amount": &lt;integer&gt;,
"lnurl_title": &lt;string&gt;}' -H "Content-type: application/json"
-H "X-Api-Key: {{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Update copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/copilot/api/v1/copilot/&lt;copilot_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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -d '{"title": &lt;string&gt;,
"animation": &lt;string&gt;, "show_message":&lt;string&gt;,
"amount": &lt;integer&gt;, "lnurl_title": &lt;string&gt;}' -H
"Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get copilot">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/copilot/api/v1/copilot/&lt;copilot_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 />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/copilot/&lt;copilot_id&gt;
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get copilots">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span> /copilot/api/v1/copilots</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Body (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;copilot_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/copilots -H "X-Api-Key: {{
user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a pay link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/copilot/api/v1/copilot/&lt;copilot_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 />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/copilot/&lt;copilot_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Trigger an animation"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/api/v1/copilot/ws/&lt;copilot_id&gt;/&lt;comment&gt;/&lt;data&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 200</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}/api/v1/copilot/ws/&lt;string,
copilot_id&gt;/&lt;string, comment&gt;/&lt;string, gif name&gt; -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
</q-card>
@@ -0,0 +1,287 @@
{% extends "public.html" %} {% block page %}<q-page>
<video
autoplay="true"
id="videoScreen"
style="width: 100%"
class="fixed-bottom-right"
></video>
<video
autoplay="true"
id="videoCamera"
style="width: 100%"
class="fixed-bottom-right"
></video>
<img src="" style="width: 100%" id="animations" class="fixed-bottom-left" />
<div
v-if="copilot.lnurl_toggle == 1"
class="rounded-borders column fixed-right"
style="
width: 250px;
background-color: white;
height: 300px;
margin-top: 10%;
"
>
<div class="col">
<qrcode
:value="copilot.lnurl"
:options="{width:250}"
class="rounded-borders"
></qrcode>
<center class="absolute-bottom" style="color: black; font-size: 20px">
{% raw %}{{ copilot.lnurl_title }}{% endraw %}
</center>
</div>
</div>
<h2
v-if="copilot.show_price != 0"
class="text-bold fixed-bottom-left"
style="
margin: 60px 60px;
font-size: 110px;
text-shadow: 4px 8px 4px black;
color: white;
"
>
{% raw %}{{ price }}{% endraw %}
</h2>
<p
v-if="copilot.show_ack != 0"
class="fixed-top"
style="
font-size: 22px;
text-shadow: 2px 4px 1px black;
color: white;
padding-left: 40%;
"
>
Powered by LNbits/StreamerCopilot
</p>
</q-page>
{% endblock %} {% block scripts %}
<style>
body.body--dark .q-drawer,
body.body--dark .q-footer,
body.body--dark .q-header,
.q-drawer,
.q-footer,
.q-header {
display: none;
}
.q-page {
padding: 0px;
}
</style>
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
price: '',
counter: 1,
colours: ['teal', 'purple', 'indigo', 'pink', 'green'],
copilot: {},
animQueue: [],
queue: false,
lnurl: ''
}
},
methods: {
showNotif: function (userMessage) {
var colour = this.colours[
Math.floor(Math.random() * this.colours.length)
]
this.$q.notify({
color: colour,
icon: 'chat_bubble_outline',
html: true,
message: '<h4 style="color: white;">' + userMessage + '</h4>',
position: 'top-left',
timeout: 5000
})
},
openURL: function (url) {
return Quasar.utils.openURL(url)
},
initCamera() {
var video = document.querySelector('#videoCamera')
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({video: true})
.then(function (stream) {
video.srcObject = stream
})
.catch(function (err0r) {
console.log('Something went wrong!')
})
}
},
initScreenShare() {
var video = document.querySelector('#videoScreen')
navigator.mediaDevices
.getDisplayMedia({video: true})
.then(function (stream) {
video.srcObject = stream
})
.catch(function (err0r) {
console.log('Something went wrong!')
})
},
pushAnim(content) {
document.getElementById('animations').style.width = content[0]
document.getElementById('animations').src = content[1]
if (content[2] != 'none') {
self.showNotif(content[2])
}
setTimeout(function () {
document.getElementById('animations').src = ''
}, 5000)
},
launch() {
self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/ws/' +
self.copilot.id +
'/launching/rocket'
)
.then(function (response1) {
self.$q.notify({
color: 'green',
message: 'Sent!'
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
},
mounted() {
this.initCamera()
},
created: function () {
self = this
self.copilot = JSON.parse(localStorage.getItem('copilot'))
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/' + self.copilot.id,
localStorage.getItem('inkey')
)
.then(function (response) {
self.copilot = response.data
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
this.connectionBitStamp = new WebSocket('wss://ws.bitstamp.net')
const obj = JSON.stringify({
event: 'bts:subscribe',
data: {channel: 'live_trades_' + self.copilot.show_price}
})
this.connectionBitStamp.onmessage = function (e) {
if (self.copilot.show_price) {
if (self.copilot.show_price == 'btcusd') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(JSON.parse(e.data).data.price)
)
} else if (self.copilot.show_price == 'btceur') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(JSON.parse(e.data).data.price)
)
} else if (self.copilot.show_price == 'btcgbp') {
self.price = String(
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'GBP'
}).format(JSON.parse(e.data).data.price)
)
}
}
}
this.connectionBitStamp.onopen = () => this.connectionBitStamp.send(obj)
const fetch = data =>
new Promise(resolve => setTimeout(resolve, 5000, this.pushAnim(data)))
const addTask = (() => {
let pending = Promise.resolve()
const run = async data => {
try {
await pending
} finally {
return fetch(data)
}
}
return data => (pending = run(data))
})()
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/copilot/ws/' +
self.copilot.id
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/copilot/ws/' +
self.copilot.id
}
this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) {
console.log(e)
res = e.data.split('-')
if (res[0] == 'rocket') {
addTask(['40%', '/copilot/static/rocket.gif', res[1]])
}
if (res[0] == 'face') {
addTask(['35%', '/copilot/static/face.gif', res[1]])
}
if (res[0] == 'bitcoin') {
addTask(['30%', '/copilot/static/bitcoin.gif', res[1]])
}
if (res[0] == 'confetti') {
addTask(['100%', '/copilot/static/confetti.gif', res[1]])
}
if (res[0] == 'martijn') {
addTask(['40%', '/copilot/static/martijn.gif', res[1]])
}
if (res[0] == 'rick') {
addTask(['40%', '/copilot/static/rick.gif', res[1]])
}
if (res[0] == 'true') {
document.getElementById('videoCamera').style.width = '20%'
self.initScreenShare()
}
if (res[0] == 'false') {
document.getElementById('videoCamera').style.width = '100%'
document.getElementById('videoScreen').src = null
}
}
this.connection.onopen = () => this.launch
}
})
</script>
{% endblock %}
@@ -0,0 +1,660 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
{% raw %}
<q-btn unelevated color="primary" @click="formDialogCopilot.show = true"
>New copilot instance
</q-btn>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Copilots</h5>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportcopilotCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="CopilotLinks"
row-key="id"
:columns="CopilotsTable.columns"
:pagination.sync="CopilotsTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th style="width: 5%"></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<q-btn
unelevated
dense
size="xs"
icon="apps"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openCopilotPanel(props.row.id)"
>
<q-tooltip> Panel </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
unelevated
dense
size="xs"
icon="face"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openCopilotCompose(props.row.id)"
>
<q-tooltip> Compose window </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="deleteCopilotLink(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete copilot </q-tooltip>
</q-btn>
</q-td>
<q-td>
<q-btn
flat
dense
size="xs"
@click="openUpdateCopilotLink(props.row.id)"
icon="edit"
color="light-blue"
>
<q-tooltip> Edit copilot </q-tooltip>
</q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</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-my-none">
{{SITE_TITLE}} StreamCopilot Extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "copilot/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="formDialogCopilot.show"
position="top"
@hide="closeFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendFormDataCopilot" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.title"
type="text"
label="Title"
></q-input>
<div class="row">
<q-checkbox
v-model="formDialogCopilot.data.lnurl_toggle"
label="Include lnurl payment QR? (requires https)"
left-label
></q-checkbox>
</div>
<div v-if="formDialogCopilot.data.lnurl_toggle">
<q-checkbox
v-model="formDialogCopilot.data.show_message"
left-label
label="Show lnurl-pay messages? (supported by few wallets)"
></q-checkbox>
<q-select
filled
dense
emit-value
v-model="formDialogCopilot.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 1"
>
<q-card>
<q-card-section>
<div class="row">
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation1"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation1threshold"
type="number"
step="1"
label="From *sats (min. 10)"
:rules="[ val => val >= 10 || 'Please use minimum 10' ]"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation1webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 2 (Must be higher than last)"
>
<q-card>
<q-card-section>
<div
class="row"
v-if="formDialogCopilot.data.animation1threshold > 0"
>
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation2"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model="formDialogCopilot.data.animation2threshold"
type="number"
step="1"
label="From *sats"
:min="formDialogCopilot.data.animation1threshold"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation2webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Payment threshold 3 (Must be higher than last)"
>
<q-card>
<q-card-section>
<div
class="row"
v-if="formDialogCopilot.data.animation2threshold > formDialogCopilot.data.animation1threshold"
>
<div class="col">
<q-select
filled
dense
v-model.trim="formDialogCopilot.data.animation3"
:options="options"
label="Animation"
/>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model="formDialogCopilot.data.animation3threshold"
type="number"
step="1"
label="From *sats"
:min="formDialogCopilot.data.animation2threshold"
>
</q-input>
</div>
<div class="col q-pl-xs">
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.animation3webhook"
type="text"
label="Webhook"
>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-input
filled
dense
v-model.trim="formDialogCopilot.data.lnurl_title"
type="text"
max="1440"
label="Lnurl title (message with QR code)"
>
</q-input>
</div>
<div class="q-gutter-sm">
<q-select
filled
dense
style="width: 50%"
v-model.trim="formDialogCopilot.data.show_price"
:options="currencyOptions"
label="Show price"
/>
</div>
<div class="q-gutter-sm">
<div class="row">
<q-checkbox
v-model="formDialogCopilot.data.show_ack"
left-label
label="Show 'powered by LNbits'"
></q-checkbox>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
v-if="formDialogCopilot.data.id"
unelevated
color="primary"
:disable="
formDialogCopilot.data.title == ''"
type="submit"
>Update Copilot</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="
formDialogCopilot.data.title == ''"
type="submit"
>Create Copilot</q-btn
>
<q-btn @click="cancelCopilot" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
var mapCopilot = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
obj.displayComposeUrl = ['/copilot/cp/', obj.id].join('')
obj.displayPanelUrl = ['/copilot/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
filter: '',
CopilotLinks: [],
CopilotLinksObj: [],
CopilotsTable: {
columns: [
{
name: 'theId',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'lnurl_toggle',
align: 'left',
label: 'Show lnurl pay link',
field: 'lnurl_toggle'
},
{
name: 'title',
align: 'left',
label: 'title',
field: 'title'
},
{
name: 'amount_made',
align: 'left',
label: 'amount made',
field: 'amount_made'
}
],
pagination: {
rowsPerPage: 10
}
},
passedCopilot: {},
formDialog: {
show: false,
data: {}
},
formDialogCopilot: {
show: false,
data: {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
qrCodeDialog: {
show: false,
data: null
},
options: ['bitcoin', 'confetti', 'rocket', 'face', 'martijn', 'rick'],
currencyOptions: ['None', 'btcusd', 'btceur', 'btcgbp']
}
},
methods: {
cancelCopilot: function (data) {
var self = this
self.formDialogCopilot.show = false
self.clearFormDialogCopilot()
},
closeFormDialog: function () {
this.clearFormDialogCopilot()
this.formDialog.data = {
is_unique: false
}
},
sendFormDataCopilot: function () {
var self = this
if (self.formDialogCopilot.data.id) {
this.updateCopilot(
self.g.user.wallets[0].adminkey,
self.formDialogCopilot.data
)
} else {
console.log(self.g.user.wallets[0].adminkey)
console.log(self.formDialogCopilot.data)
this.createCopilot(
self.g.user.wallets[0].adminkey,
self.formDialogCopilot.data
)
}
},
createCopilot: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
if (property == 'animation1threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation2threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation3threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
}
LNbits.api
.request('POST', '/copilot/api/v1/copilot', wallet, updatedData)
.then(function (response) {
self.CopilotLinks.push(mapCopilot(response.data))
self.formDialogCopilot.show = false
self.clearFormDialogCopilot()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getCopilots: function () {
var self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot',
this.g.user.wallets[0].inkey
)
.then(function (response) {
if (response.data) {
self.CopilotLinks = response.data.map(mapCopilot)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getCopilot: function (copilot_id) {
var self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/' + copilot_id,
this.g.user.wallets[0].inkey
)
.then(function (response) {
localStorage.setItem('copilot', JSON.stringify(response.data))
localStorage.setItem('inkey', self.g.user.wallets[0].inkey)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
openCopilotCompose: function (copilot_id) {
this.getCopilot(copilot_id)
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
open('../copilot/cp/', '_blank', params)
},
openCopilotPanel: function (copilot_id) {
this.getCopilot(copilot_id)
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=300,height=450,left=10,top=400'
open('../copilot/pn/', '_blank', params)
},
deleteCopilotLink: function (copilotId) {
var self = this
var link = _.findWhere(this.CopilotLinks, {id: copilotId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this pay link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/copilot/api/v1/copilot/' + copilotId,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
return obj.id === copilotId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
openUpdateCopilotLink: function (copilotId) {
var self = this
var copilot = _.findWhere(this.CopilotLinks, {id: copilotId})
self.formDialogCopilot.data = _.clone(copilot._data)
self.formDialogCopilot.show = true
},
updateCopilot: function (wallet, data) {
var self = this
var updatedData = {}
for (const property in data) {
if (data[property]) {
updatedData[property] = data[property]
}
if (property == 'animation1threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation2threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
if (property == 'animation3threshold' && data[property]) {
updatedData[property] = parseInt(data[property])
}
}
LNbits.api
.request(
'PUT',
'/copilot/api/v1/copilot/' + updatedData.id,
wallet,
updatedData
)
.then(function (response) {
self.CopilotLinks = _.reject(self.CopilotLinks, function (obj) {
return obj.id === updatedData.id
})
self.CopilotLinks.push(mapCopilot(response.data))
self.formDialogCopilot.show = false
self.clearFormDialogCopilot()
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
clearFormDialogCopilot() {
this.formDialogCopilot.data = {
lnurl_toggle: false,
show_message: false,
show_ack: false,
show_price: 'None',
title: ''
}
},
exportcopilotCSV: function () {
var self = this
LNbits.utils.exportCSV(self.CopilotsTable.columns, this.CopilotLinks)
}
},
created: function () {
var self = this
var getCopilots = this.getCopilots
getCopilots()
}
})
</script>
{% endblock %}
@@ -0,0 +1,156 @@
{% extends "public.html" %} {% block page %}
<div class="q-pa-sm" style="width: 240px; margin: 10px auto">
<q-card class="my-card">
<div class="column">
<div class="col">
<center>
<q-btn
flat
round
dense
@click="openCompose"
icon="face"
style="font-size: 60px"
></q-btn>
</center>
</div>
<center>
<div class="col" style="margin: 15px; font-size: 22px">
Title: {% raw %} {{ copilot.title }} {% endraw %}
</div>
</center>
<q-separator></q-separator>
<div class="col">
<div class="row">
<div class="col">
<q-btn
class="q-mt-sm q-ml-sm"
color="primary"
@click="fullscreenToggle"
label="Screen share"
size="sm"
>
</q-btn>
</div>
</div>
<div class="row q-pa-sm">
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('rocket')"
label="rocket"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('confetti')"
label="confetti"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('face')"
label="face"
size="sm"
/>
</div>
</div>
<div class="row q-pa-sm">
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('rick')"
label="rick"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('martijn')"
label="martijn"
size="sm"
/>
</div>
<div class="col">
<q-btn
style="width: 95%"
color="primary"
@click="animationBTN('bitcoin')"
label="bitcoin"
size="sm"
/>
</div>
</div>
</div>
</div>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
fullscreen_cam: true,
textareaModel: '',
iframe: '',
copilot: {}
}
},
methods: {
iframeChange: function (url) {
this.connection.send(String(url))
},
fullscreenToggle: function () {
self = this
self.animationBTN(String(this.fullscreen_cam))
if (this.fullscreen_cam) {
this.fullscreen_cam = false
} else {
this.fullscreen_cam = true
}
},
openCompose: function () {
let params =
'scrollbars=no, resizable=no,status=no,location=no,toolbar=no,menubar=no,width=1200,height=644,left=410,top=100'
open('../cp/', 'test', params)
},
animationBTN: function (name) {
self = this
LNbits.api
.request(
'GET',
'/copilot/api/v1/copilot/ws/' + self.copilot.id + '/none/' + name
)
.then(function (response1) {
self.$q.notify({
color: 'green',
message: 'Sent!'
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
},
created: function () {
self = this
self.copilot = JSON.parse(localStorage.getItem('copilot'))
}
})
</script>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
from typing import List
from fastapi import Request, WebSocket, WebSocketDisconnect
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse # type: ignore
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import copilot_ext, copilot_renderer
from .crud import get_copilot
templates = Jinja2Templates(directory="templates")
@copilot_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return copilot_renderer().TemplateResponse(
"copilot/index.html", {"request": request, "user": user.dict()}
)
@copilot_ext.get("/cp/", response_class=HTMLResponse)
async def compose(request: Request):
return copilot_renderer().TemplateResponse(
"copilot/compose.html", {"request": request}
)
@copilot_ext.get("/pn/", response_class=HTMLResponse)
async def panel(request: Request):
return copilot_renderer().TemplateResponse(
"copilot/panel.html", {"request": request}
)
##################WEBSOCKET ROUTES########################
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, copilot_id: str):
await websocket.accept()
websocket.id = copilot_id
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, copilot_id: str):
for connection in self.active_connections:
if connection.id == copilot_id:
await connection.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@copilot_ext.websocket("/copilot/ws/{copilot_id}", name="copilot.websocket_by_id")
async def websocket_endpoint(websocket: WebSocket, copilot_id: str):
await manager.connect(websocket, copilot_id)
try:
while True:
data = await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
async def updater(copilot_id, data, comment):
copilot = await get_copilot(copilot_id)
if not copilot:
return
await manager.send_personal_message(f"{data + '-' + comment}", copilot_id)
+97
View File
@@ -0,0 +1,97 @@
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.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import copilot_ext
from .crud import (
create_copilot,
delete_copilot,
get_copilot,
get_copilots,
update_copilot,
)
from .models import CreateCopilotData
from .views import updater
#######################COPILOT##########################
@copilot_ext.get("/api/v1/copilot")
async def api_copilots_retrieve(
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_user = wallet.wallet.user
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
try:
return copilots
except:
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No copilots")
@copilot_ext.get("/api/v1/copilot/{copilot_id}")
async def api_copilot_retrieve(
req: Request,
copilot_id: str = Query(None),
wallet: WalletTypeInfo = Depends(get_key_type),
):
copilot = await get_copilot(copilot_id)
if not copilot:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
)
if not copilot.lnurl_toggle:
return copilot.dict()
return {**copilot.dict(), **{"lnurl": copilot.lnurl(req)}}
@copilot_ext.post("/api/v1/copilot")
@copilot_ext.put("/api/v1/copilot/{juke_id}")
async def api_copilot_create_or_update(
data: CreateCopilotData,
copilot_id: str = Query(None),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
data.user = wallet.wallet.user
data.wallet = wallet.wallet.id
if copilot_id:
copilot = await update_copilot(data, copilot_id=copilot_id)
else:
copilot = await create_copilot(data, inkey=wallet.wallet.inkey)
return copilot
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
async def api_copilot_delete(
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
):
copilot = await get_copilot(copilot_id)
if not copilot:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
)
await delete_copilot(copilot_id)
return "", HTTPStatus.NO_CONTENT
@copilot_ext.get("/api/v1/copilot/ws/{copilot_id}/{comment}/{data}")
async def api_copilot_ws_relay(
copilot_id: str = Query(None), comment: str = Query(None), data: str = Query(None)
):
copilot = await get_copilot(copilot_id)
if not copilot:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Copilot does not exist"
)
try:
await updater(copilot_id, data, comment)
except:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your copilot")
return ""
-10
View File
@@ -1,10 +0,0 @@
<h1>Diagon Alley</h1>
<h2>A movable market stand</h2>
Make a list of products to sell, point the list to an indexer (or many), stack sats.
Diagon Alley is a movable market stand, for anon transactions. You then give permission for an indexer to list those products. Delivery addresses are sent through the Lightning Network.
<img src="https://i.imgur.com/P1tvBSG.png">
<h2>API endpoints</h2>
<code>curl -X GET http://YOUR-TOR-ADDRESS</code>
-10
View File
@@ -1,10 +0,0 @@
from quart import Blueprint
diagonalley_ext: Blueprint = Blueprint(
"diagonalley", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
@@ -1,6 +0,0 @@
{
"name": "Diagon Alley",
"short_description": "Movable anonymous market stand",
"icon": "add_shopping_cart",
"contributors": ["benarc"]
}
-295
View File
@@ -1,295 +0,0 @@
from base64 import urlsafe_b64encode
from uuid import uuid4
from typing import List, Optional, Union
import httpx
from lnbits.db import open_ext_db
from lnbits.settings import WALLET
from .models import Products, Orders, Indexers
import re
regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
r"localhost|"
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
r"(?::\d+)?"
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
###Products
def create_diagonalleys_product(
*,
wallet_id: str,
product: str,
categories: str,
description: str,
image: str,
price: int,
quantity: int,
) -> Products:
with open_ext_db("diagonalley") as db:
product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
db.execute(
"""
INSERT INTO products (id, wallet, product, categories, description, image, price, quantity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
product_id,
wallet_id,
product,
categories,
description,
image,
price,
quantity,
),
)
return get_diagonalleys_product(product_id)
def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db:
db.execute(
f"UPDATE products SET {q} WHERE id = ?", (*kwargs.values(), product_id)
)
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
return get_diagonalleys_indexer(product_id)
def get_diagonalleys_product(product_id: str) -> Optional[Products]:
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM products WHERE id = ?", (product_id,))
return Products(**row) if row else None
def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Products]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(
f"SELECT * FROM products WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Products(**row) for row in rows]
def delete_diagonalleys_product(product_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM products WHERE id = ?", (product_id,))
###Indexers
def create_diagonalleys_indexer(
wallet_id: str,
shopname: str,
indexeraddress: str,
shippingzone1: str,
shippingzone2: str,
zone1cost: int,
zone2cost: int,
email: str,
) -> Indexers:
with open_ext_db("diagonalley") as db:
indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
db.execute(
"""
INSERT INTO indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
indexer_id,
wallet_id,
shopname,
indexeraddress,
False,
0,
shippingzone1,
shippingzone2,
zone1cost,
zone2cost,
email,
),
)
return get_diagonalleys_indexer(indexer_id)
def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("diagonalley") as db:
db.execute(
f"UPDATE indexers SET {q} WHERE id = ?", (*kwargs.values(), indexer_id)
)
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
return get_diagonalleys_indexer(indexer_id)
def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]:
with open_ext_db("diagonalley") as db:
roww = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
try:
x = httpx.get(roww["indexeraddress"] + "/" + roww["ratingkey"])
if x.status_code == 200:
print(x)
print("poo")
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE indexers SET online = ? WHERE id = ?",
(
True,
indexer_id,
),
)
else:
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE indexers SET online = ? WHERE id = ?",
(
False,
indexer_id,
),
)
except:
print("An exception occurred")
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
return Indexers(**row) if row else None
def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexers]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(
f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows:
try:
x = httpx.get(r["indexeraddress"] + "/" + r["ratingkey"])
if x.status_code == 200:
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE indexers SET online = ? WHERE id = ?",
(
True,
r["id"],
),
)
else:
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE indexers SET online = ? WHERE id = ?",
(
False,
r["id"],
),
)
except:
print("An exception occurred")
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(
f"SELECT * FROM indexers WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Indexers(**row) for row in rows]
def delete_diagonalleys_indexer(indexer_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM indexers WHERE id = ?", (indexer_id,))
###Orders
def create_diagonalleys_order(
*,
productid: str,
wallet: str,
product: str,
quantity: int,
shippingzone: str,
address: str,
email: str,
invoiceid: str,
paid: bool,
shipped: bool,
) -> Indexers:
with open_ext_db("diagonalley") as db:
order_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
db.execute(
"""
INSERT INTO orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
order_id,
productid,
wallet,
product,
quantity,
shippingzone,
address,
email,
invoiceid,
False,
False,
),
)
return get_diagonalleys_order(order_id)
def get_diagonalleys_order(order_id: str) -> Optional[Orders]:
with open_ext_db("diagonalley") as db:
row = db.fetchone("SELECT * FROM orders WHERE id = ?", (order_id,))
return Orders(**row) if row else None
def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("diagonalley") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(
f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)
)
for r in rows:
PAID = (await WALLET.get_invoice_status(r["invoiceid"])).paid
if PAID:
with open_ext_db("diagonalley") as db:
db.execute(
"UPDATE orders SET paid = ? WHERE id = ?",
(
True,
r["id"],
),
)
rows = db.fetchall(
f"SELECT * FROM orders WHERE wallet IN ({q})", (*wallet_ids,)
)
return [Orders(**row) for row in rows]
def delete_diagonalleys_order(order_id: str) -> None:
with open_ext_db("diagonalley") as db:
db.execute("DELETE FROM orders WHERE id = ?", (order_id,))
@@ -1,60 +0,0 @@
async def m001_initial(db):
"""
Initial products table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
product TEXT NOT NULL,
categories TEXT NOT NULL,
description TEXT NOT NULL,
image TEXT NOT NULL,
price INTEGER NOT NULL,
quantity INTEGER NOT NULL
);
"""
)
"""
Initial indexers table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS indexers (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
shopname TEXT NOT NULL,
indexeraddress TEXT NOT NULL,
online BOOLEAN NOT NULL,
rating INTEGER NOT NULL,
shippingzone1 TEXT NOT NULL,
shippingzone2 TEXT NOT NULL,
zone1cost INTEGER NOT NULL,
zone2cost INTEGER NOT NULL,
email TEXT NOT NULL
);
"""
)
"""
Initial orders table.
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
productid TEXT NOT NULL,
wallet TEXT NOT NULL,
product TEXT NOT NULL,
quantity INTEGER NOT NULL,
shippingzone INTEGER NOT NULL,
address TEXT NOT NULL,
email TEXT NOT NULL,
invoiceid TEXT NOT NULL,
paid BOOLEAN NOT NULL,
shipped BOOLEAN NOT NULL
);
"""
)
-40
View File
@@ -1,40 +0,0 @@
from typing import NamedTuple
class Indexers(NamedTuple):
id: str
wallet: str
shopname: str
indexeraddress: str
online: bool
rating: str
shippingzone1: str
shippingzone2: str
zone1cost: int
zone2cost: int
email: str
class Products(NamedTuple):
id: str
wallet: str
product: str
categories: str
description: str
image: str
price: int
quantity: int
class Orders(NamedTuple):
id: str
productid: str
wallet: str
product: str
quantity: int
shippingzone: int
address: str
email: str
invoiceid: str
paid: bool
shipped: bool
@@ -1,122 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="Info"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<h5 class="text-subtitle1 q-my-none">
Diagon Alley: Decentralised Market-Stalls
</h5>
<p>
Make a list of products to sell, point your list of products at a public
indexer. Buyers browse your products on the indexer, and pay you
directly. Ratings are managed by the indexer. Your stall can be listed
in multiple indexers, even over TOR, if you wish to be anonymous.<br />
More information on the
<a href="https://github.com/lnbits/Diagon-Alley"
>Diagon Alley Protocol</a
><br />
<small>
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
>
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item
group="api"
dense
expand-separator
label="Get prodcuts, categorised by wallet"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>Product JSON list</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/products/&lt;indexer_id&gt;</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get invoice for product"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/api/v1/diagonalley/stall/order/&lt;indexer_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"id": &lt;string&gt;, "address": &lt;string&gt;, "shippingzone":
&lt;integer&gt;, "email": &lt;string&gt;, "quantity":
&lt;integer&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"checking_id": &lt;string&gt;,"payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/order/&lt;indexer_id&gt; -d
'{"id": &lt;product_id&&gt;, "email": &lt;customer_email&gt;,
"address": &lt;customer_address&gt;, "quantity": 2, "shippingzone":
1}' -H "Content-type: application/json"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check a product has been shipped"
class="q-mb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"shipped": &lt;boolean&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}diagonalley/api/v1/diagonalley/stall/checkshipped/&lt;checking_id&gt;
-H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>
@@ -1,906 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="deep-purple" @click="productDialog.show = true"
>New Product</q-btn
>
<q-btn unelevated color="deep-purple" @click="indexerDialog.show = true"
>New Indexer
<q-tooltip>
Frontend shop your stall will list its products in
</q-tooltip></q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Products</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportProductsCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="products"
row-key="id"
:columns="productsTable.columns"
:pagination.sync="productsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="add_shopping_cart"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.wallet"
target="_blank"
></q-btn>
<q-tooltip> Link to pass to stall indexer </q-tooltip>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openProductUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteProduct(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Indexers</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportIndexersCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="indexers"
row-key="id"
:columns="indexersTable.columns"
:pagination.sync="indexersTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="openIndexerUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteIndexer(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Orders</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportOrdersCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
dense
flat
:data="orders"
row-key="id"
:columns="ordersTable.columns"
:pagination.sync="ordersTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="shipOrder(props.row.id)"
icon="add_shopping_cart"
color="green"
>
<q-tooltip> Product shipped? </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteOrder(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">LNbits Diagon Alley Extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "diagonalley/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="productDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendProductFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="productDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="productDialog.data.product"
label="Product"
></q-input>
<q-input
filled
dense
v-model.trim="productDialog.data.categories"
placeholder="cakes, guns, wool, drugs"
label="Categories seperated by comma"
></q-input>
<q-input
filled
dense
v-model.trim="productDialog.data.description"
label="Description"
></q-input>
<q-input
filled
dense
v-model.trim="productDialog.data.image"
label="Image"
placeholder="Imagur link (max 500/500px)"
></q-input>
<q-input
filled
dense
v-model.number="productDialog.data.price"
type="number"
label="Price"
></q-input>
<q-input
filled
dense
v-model.number="productDialog.data.quantity"
type="number"
label="Quantity"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="productDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update Product</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="productDialog.data.image == null
|| productDialog.data.product == null
|| productDialog.data.description == null
|| productDialog.data.quantity == null"
type="submit"
>Create Product</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="indexerDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendIndexerFormData" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="indexerDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<q-input
filled
dense
v-model.trim="indexerDialog.data.shopname"
label="Shop Name"
></q-input>
<q-input
filled
dense
v-model.trim="indexerDialog.data.indexeraddress"
label="Shop link (LNbits will point to)"
></q-input>
<q-select
filled
dense
v-model.trim="indexerDialog.data.shippingzone1"
multiple
:options="shippingoptions"
label="Shipping Zone 1"
></q-select>
<q-input
filled
dense
v-model.number="indexerDialog.data.zone1cost"
type="number"
label="Zone 1 Cost"
></q-input>
<q-select
filled
dense
v-model.trim="indexerDialog.data.shippingzone2"
multiple
:options="shippingoptions"
label="Shipping Zone 2"
></q-select>
<q-input
filled
dense
v-model.number="indexerDialog.data.zone2cost"
type="number"
label="Zone 2 Cost"
></q-input>
<q-input
filled
dense
v-model.trim="indexerDialog.data.email"
label="Email to share with customers"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="indexerDialog.data.id"
unelevated
color="deep-purple"
type="submit"
>Update Indexer</q-btn
>
<q-btn
v-else
unelevated
color="deep-purple"
:disable="indexerDialog.data.shopname == null
|| indexerDialog.data.shippingzone1 == null
|| indexerDialog.data.indexeraddress == null
|| indexerDialog.data.zone1cost == null
|| indexerDialog.data.shippingzone2 == null
|| indexerDialog.data.zone2cost == null
|| indexerDialog.data.email == null"
type="submit"
>Create Indexer</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapDiagonAlley = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.wall = ['/diagonalley/', obj.id].join('')
obj._data = _.clone(obj)
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
products: [],
orders: [],
indexers: [],
shippedModel: false,
shippingoptions: [
'Australia',
'Austria',
'Belgium',
'Brazil',
'Canada',
'Denmark',
'Finland',
'France*',
'Germany',
'Greece',
'Hong Kong',
'Hungary',
'Ireland',
'Indonesia',
'Israel',
'Italy',
'Japan',
'Kazakhstan',
'Korea',
'Luxembourg',
'Malaysia',
'Mexico',
'Netherlands',
'New Zealand',
'Norway',
'Poland',
'Portugal',
'Russia',
'Saudi Arabia',
'Singapore',
'Spain',
'Sweden',
'Switzerland',
'Thailand',
'Turkey',
'Ukraine',
'United Kingdom**',
'United States***',
'Vietnam',
'China'
],
label: '',
indexersTable: {
columns: [
{name: 'shopname', align: 'left', label: 'Shop', field: 'shopname'},
{
name: 'indexeraddress',
align: 'left',
label: 'Address',
field: 'indexeraddress'
},
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{
name: 'rating',
align: 'left',
label: 'Your Rating',
field: 'rating'
},
{name: 'email', align: 'left', label: 'Your email', field: 'email'},
{name: 'online', align: 'left', label: 'Online', field: 'online'}
],
pagination: {
rowsPerPage: 10
}
},
ordersTable: {
columns: [
{
name: 'product',
align: 'left',
label: 'Product',
field: 'product'
},
{
name: 'quantity',
align: 'left',
label: 'Quantity',
field: 'quantity'
},
{
name: 'address',
align: 'left',
label: 'Address',
field: 'address'
},
{
name: 'invoiceid',
align: 'left',
label: 'InvoiceID',
field: 'invoiceid'
},
{name: 'paid', align: 'left', label: 'Paid', field: 'paid'},
{name: 'shipped', align: 'left', label: 'Shipped', field: 'shipped'}
],
pagination: {
rowsPerPage: 10
}
},
productsTable: {
columns: [
{
name: 'product',
align: 'left',
label: 'Product',
field: 'product'
},
{
name: 'description',
align: 'left',
label: 'Description',
field: 'description'
},
{
name: 'categories',
align: 'left',
label: 'Categories',
field: 'categories'
},
{name: 'image', align: 'left', label: 'Image', field: 'image'},
{name: 'price', align: 'left', label: 'Price', field: 'price'},
{
name: 'quantity',
align: 'left',
label: 'Quantity',
field: 'quantity'
},
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}
],
pagination: {
rowsPerPage: 10
}
},
productDialog: {
show: false,
data: {}
},
orderDialog: {
show: false,
data: {}
},
indexerDialog: {
show: false,
data: {}
}
}
},
methods: {
getIndexers: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/diagonalley/indexers?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.indexers = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
openIndexerUpdateDialog: function (linkId) {
var link = _.findWhere(this.indexers, {id: linkId})
this.indexerDialog.data = _.clone(link._data)
this.indexerDialog.show = true
},
sendIndexerFormData: function () {
if (this.indexerDialog.data.id) {
} else {
var data = {
shopname: this.indexerDialog.data.shopname,
indexeraddress: this.indexerDialog.data.indexeraddress,
shippingzone1: this.indexerDialog.data.shippingzone1.join(', '),
zone1cost: this.indexerDialog.data.zone1cost,
shippingzone2: this.indexerDialog.data.shippingzone2.join(', '),
zone2cost: this.indexerDialog.data.zone2cost,
email: this.indexerDialog.data.email
}
}
if (this.indexerDialog.data.id) {
this.updateIndexer(this.indexerDialog.data)
} else {
this.createIndexer(data)
}
},
updateIndexer: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/diagonalley/indexers' + data.id,
_.findWhere(this.g.user.wallets, {
id: this.indexerDialog.data.wallet
}).inkey,
_.pick(
data,
'shopname',
'indexeraddress',
'shippingzone1',
'zone1cost',
'shippingzone2',
'zone2cost',
'email'
)
)
.then(function (response) {
self.indexers = _.reject(self.indexers, function (obj) {
return obj.id == data.id
})
self.indexers.push(mapDiagonAlley(response.data))
self.indexerDialog.show = false
self.indexerDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createIndexer: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/diagonalley/indexers',
_.findWhere(this.g.user.wallets, {
id: this.indexerDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.indexers.push(mapDiagonAlley(response.data))
self.indexerDialog.show = false
self.indexerDialog.data = {}
data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteIndexer: function (indexerId) {
var self = this
var indexer = _.findWhere(this.indexers, {id: indexerId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this Indexer link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/indexers/' + indexerId,
_.findWhere(self.g.user.wallets, {id: indexer.wallet}).inkey
)
.then(function (response) {
self.indexers = _.reject(self.indexers, function (obj) {
return obj.id == indexerId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportIndexersCSV: function () {
LNbits.utils.exportCSV(this.indexersTable.columns, this.indexers)
},
getOrders: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/diagonalley/orders?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
createOrder: function () {
var data = {
address: this.orderDialog.data.address,
email: this.orderDialog.data.email,
quantity: this.orderDialog.data.quantity,
shippingzone: this.orderDialog.data.shippingzone
}
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/diagonalley/orders',
_.findWhere(this.g.user.wallets, {id: this.orderDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.orders.push(mapDiagonAlley(response.data))
self.orderDialog.show = false
self.orderDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteOrder: function (orderId) {
var self = this
var order = _.findWhere(this.orders, {id: orderId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this order link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/orders/' + orderId,
_.findWhere(self.g.user.wallets, {id: order.wallet}).inkey
)
.then(function (response) {
self.orders = _.reject(self.orders, function (obj) {
return obj.id == orderId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
shipOrder: function (order_id) {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/diagonalley/orders/shipped/' + order_id,
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.orders = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
exportOrdersCSV: function () {
LNbits.utils.exportCSV(this.ordersTable.columns, this.orders)
},
getProducts: function () {
var self = this
LNbits.api
.request(
'GET',
'/diagonalley/api/v1/diagonalley/products?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.products = response.data.map(function (obj) {
return mapDiagonAlley(obj)
})
})
},
openProductUpdateDialog: function (linkId) {
var link = _.findWhere(this.products, {id: linkId})
this.productDialog.data = _.clone(link._data)
this.productDialog.show = true
},
sendProductFormData: function () {
if (this.productDialog.data.id) {
} else {
var data = {
product: this.productDialog.data.product,
categories: this.productDialog.data.categories,
description: this.productDialog.data.description,
image: this.productDialog.data.image,
price: this.productDialog.data.price,
quantity: this.productDialog.data.quantity
}
}
if (this.productDialog.data.id) {
this.updateProduct(this.productDialog.data)
} else {
this.createProduct(data)
}
},
updateProduct: function (data) {
var self = this
LNbits.api
.request(
'PUT',
'/diagonalley/api/v1/diagonalley/products' + data.id,
_.findWhere(this.g.user.wallets, {
id: this.productDialog.data.wallet
}).inkey,
_.pick(
data,
'shopname',
'indexeraddress',
'shippingzone1',
'zone1cost',
'shippingzone2',
'zone2cost',
'email'
)
)
.then(function (response) {
self.products = _.reject(self.products, function (obj) {
return obj.id == data.id
})
self.products.push(mapDiagonAlley(response.data))
self.productDialog.show = false
self.productDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createProduct: function (data) {
var self = this
LNbits.api
.request(
'POST',
'/diagonalley/api/v1/diagonalley/products',
_.findWhere(this.g.user.wallets, {
id: this.productDialog.data.wallet
}).inkey,
data
)
.then(function (response) {
self.products.push(mapDiagonAlley(response.data))
self.productDialog.show = false
self.productDialog.data = {}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteProduct: function (productId) {
var self = this
var product = _.findWhere(this.products, {id: productId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this products link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/diagonalley/api/v1/diagonalley/products/' + productId,
_.findWhere(self.g.user.wallets, {id: product.wallet}).inkey
)
.then(function (response) {
self.products = _.reject(self.products, function (obj) {
return obj.id == productId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportProductsCSV: function () {
LNbits.utils.exportCSV(this.productsTable.columns, this.products)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getProducts()
this.getOrders()
this.getIndexers()
}
}
})
</script>
{% endblock %}
@@ -1,3 +0,0 @@
<script>
console.log('{{ stall }}')
</script>
-11
View File
@@ -1,11 +0,0 @@
from quart import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext
@diagonalley_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("diagonalley/index.html", user=g.user)

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