Browse Source

Merge pull request #93 from lnbits/quart

aiosqlite
fiatjaf 4 years ago
committed by GitHub
parent
commit
dbabf937c4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .env.example
  2. 1
      .gitignore
  3. 2
      Dockerfile
  4. 5
      Makefile
  5. 14
      Pipfile
  6. 311
      Pipfile.lock
  7. 2
      Procfile
  8. 4
      app.json
  9. 8
      docs/devs/installation.md
  10. 7
      lnbits/__main__.py
  11. 92
      lnbits/app.py
  12. 33
      lnbits/commands.py
  13. 6
      lnbits/core/__init__.py
  14. 2
      lnbits/core/crud.py
  15. 2
      lnbits/core/services.py
  16. 7
      lnbits/core/templates/core/extensions.html
  17. 7
      lnbits/core/templates/core/index.html
  18. 20
      lnbits/core/templates/core/wallet.html
  19. 16
      lnbits/core/views/api.py
  20. 22
      lnbits/core/views/generic.py
  21. 4
      lnbits/core/views/lnurl.py
  22. 21
      lnbits/decorators.py
  23. 6
      lnbits/ext.py
  24. 2
      lnbits/extensions/amilk/__init__.py
  25. 11
      lnbits/extensions/amilk/views.py
  26. 10
      lnbits/extensions/amilk/views_api.py
  27. 2
      lnbits/extensions/diagonalley/__init__.py
  28. 10
      lnbits/extensions/diagonalley/views.py
  29. 32
      lnbits/extensions/diagonalley/views_api.py
  30. 2
      lnbits/extensions/events/__init__.py
  31. 1
      lnbits/extensions/events/templates/events/display.html
  32. 17
      lnbits/extensions/events/templates/events/register.html
  33. 1
      lnbits/extensions/events/templates/events/ticket.html
  34. 28
      lnbits/extensions/events/views.py
  35. 20
      lnbits/extensions/events/views_api.py
  36. 2
      lnbits/extensions/example/__init__.py
  37. 6
      lnbits/extensions/example/views.py
  38. 4
      lnbits/extensions/example/views_api.py
  39. 2
      lnbits/extensions/lndhub/__init__.py
  40. 6
      lnbits/extensions/lndhub/decorators.py
  41. 7
      lnbits/extensions/lndhub/templates/lndhub/index.html
  42. 6
      lnbits/extensions/lndhub/views.py
  43. 24
      lnbits/extensions/lndhub/views_api.py
  44. 2
      lnbits/extensions/lnticket/__init__.py
  45. 1
      lnbits/extensions/lnticket/templates/lnticket/display.html
  46. 10
      lnbits/extensions/lnticket/views.py
  47. 16
      lnbits/extensions/lnticket/views_api.py
  48. 2
      lnbits/extensions/lnurlp/__init__.py
  49. 2
      lnbits/extensions/lnurlp/models.py
  50. 1
      lnbits/extensions/lnurlp/templates/lnurlp/display.html
  51. 1
      lnbits/extensions/lnurlp/templates/lnurlp/index.html
  52. 1
      lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html
  53. 16
      lnbits/extensions/lnurlp/views.py
  54. 14
      lnbits/extensions/lnurlp/views_api.py
  55. 2
      lnbits/extensions/paywall/__init__.py
  56. 1
      lnbits/extensions/paywall/templates/paywall/display.html
  57. 11
      lnbits/extensions/paywall/views.py
  58. 12
      lnbits/extensions/paywall/views_api.py
  59. 2
      lnbits/extensions/tpos/__init__.py
  60. 1
      lnbits/extensions/tpos/templates/tpos/tpos.html
  61. 10
      lnbits/extensions/tpos/views.py
  62. 12
      lnbits/extensions/tpos/views_api.py
  63. 2
      lnbits/extensions/usermanager/__init__.py
  64. 9
      lnbits/extensions/usermanager/views.py
  65. 27
      lnbits/extensions/usermanager/views_api.py
  66. 2
      lnbits/extensions/withdraw/__init__.py
  67. 2
      lnbits/extensions/withdraw/models.py
  68. 2
      lnbits/extensions/withdraw/templates/withdraw/display.html
  69. 7
      lnbits/extensions/withdraw/templates/withdraw/index.html
  70. 12
      lnbits/extensions/withdraw/templates/withdraw/print_qr.html
  71. 16
      lnbits/extensions/withdraw/views.py
  72. 16
      lnbits/extensions/withdraw/views_api.py
  73. 52
      lnbits/helpers.py
  74. 100
      lnbits/proxy_fix.py
  75. 6
      lnbits/settings.py
  76. 1
      lnbits/static/css/.gitignore
  77. 77
      lnbits/static/css/base.css
  78. 393
      lnbits/static/vendor/bolt11/decoder.js
  79. 96
      lnbits/static/vendor/bolt11/utils.js
  80. 39
      lnbits/templates/base.html
  81. 21
      lnbits/templates/print.html
  82. 44
      requirements.txt
  83. 5
      tests/conftest.py
  84. 10
      tests/core/test_views.py

10
.env.example

@ -1,12 +1,16 @@
FLASK_APP=lnbits.app QUART_APP=lnbits.app
FLASK_ENV=development QUART_ENV=development
QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
LNBITS_SITE_TITLE=LNbits LNBITS_SITE_TITLE=LNbits
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="/your_custom_data_folder" #IMPORTANT! i.e. "/home/satoshi/lnbits/lnbits/data" LNBITS_DATA_FOLDER="/your_custom_data_folder" #IMPORTANT! i.e. "/home/satoshi/lnbits/lnbits/data"
LNBITS_DISABLED_EXTENSIONS="amilk" LNBITS_DISABLED_EXTENSIONS="amilk"
LNBITS_FORCE_HTTPS=1 LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0" LNBITS_SERVICE_FEE="0.0"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), LndRestWallet, CLightningWallet, LnbitsWallet

1
.gitignore

@ -30,3 +30,4 @@ venv
__bundle__ __bundle__
node_modules node_modules
lnbits/static/bundle.*

2
Dockerfile

@ -3,7 +3,7 @@ FROM python:3.7-slim
WORKDIR /app WORKDIR /app
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install --no-cache-dir -q -r requirements.txt RUN pip install --no-cache-dir -q -r requirements.txt
RUN pip install --no-cache-dir -q gunicorn gevent RUN pip install --no-cache-dir -q hypercorn
COPY . /app COPY . /app
EXPOSE 5000 EXPOSE 5000

5
Makefile

@ -1,4 +1,4 @@
all: format check all: format check requirements.txt
format: prettier black format: prettier black
@ -18,3 +18,6 @@ checkprettier: $(shell find lnbits -name "*.js" -name ".html")
checkblack: $(shell find lnbits -name "*.py") checkblack: $(shell find lnbits -name "*.py")
./venv/bin/black --check lnbits ./venv/bin/black --check lnbits
requirements.txt: Pipfile.lock
cat Pipfile.lock | jq -r '.default | map_values(.version) | to_entries | map("\(.key)\(.value)") | join("\n")' > requirements.txt

14
Pipfile

@ -11,19 +11,19 @@ bitstring = "*"
cerberus = "*" cerberus = "*"
ecdsa = "*" ecdsa = "*"
environs = "*" environs = "*"
flask = "*"
flask-assets = "*"
flask-compress = "*"
flask-cors = "*"
flask-talisman = "*"
lnurl = "*" lnurl = "*"
pyscss = "*" pyscss = "*"
requests = "*" requests = "*"
shortuuid = "*" shortuuid = "*"
quart = "*"
quart-cors = "*"
quart-compress = "*"
secure = "*"
typing-extensions = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"
flake8 = "*"
flake8-mypy = "*"
pytest = "*" pytest = "*"
pytest-cov = "*" pytest-cov = "*"
pytest-asyncio = "*"
mypy = "==0.761"

311
Pipfile.lock

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "2270f2525e54e976b09491e458033d25ec5bbdea9e74d417e787df33031c6948" "sha256": "775e2ea809508c83df4696bba3a38a03e288cba22f0a7f562120230f40351ab9"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -16,6 +16,13 @@
] ]
}, },
"default": { "default": {
"aiofiles": {
"hashes": [
"sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb",
"sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af"
],
"version": "==0.5.0"
},
"bech32": { "bech32": {
"hashes": [ "hashes": [
"sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899", "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899",
@ -31,6 +38,12 @@
"index": "pypi", "index": "pypi",
"version": "==3.1.7" "version": "==3.1.7"
}, },
"blinker": {
"hashes": [
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
],
"version": "==1.4"
},
"brotli": { "brotli": {
"hashes": [ "hashes": [
"sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8", "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8",
@ -108,44 +121,41 @@
"index": "pypi", "index": "pypi",
"version": "==8.0.0" "version": "==8.0.0"
}, },
"flask": { "h11": {
"hashes": [ "hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", "sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" "sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502"
], ],
"index": "pypi", "version": "==0.10.0"
"version": "==1.1.2"
}, },
"flask-assets": { "h2": {
"hashes": [ "hashes": [
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2", "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5",
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b" "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"
], ],
"index": "pypi", "version": "==3.2.0"
"version": "==2.0"
}, },
"flask-compress": { "hpack": {
"hashes": [ "hashes": [
"sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c" "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
], ],
"index": "pypi", "version": "==3.0.0"
"version": "==1.5.0"
}, },
"flask-cors": { "hypercorn": {
"hashes": [ "hashes": [
"sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8", "sha256:19f32e7267225c8108ad585b2c5deddf1fe75950797a0e87a682a3a00ef1af95",
"sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324" "sha256:809d77f3bf9fa0794a598d8dfa0f8d889e7e1c2f927581cd33068803169dc474"
], ],
"index": "pypi", "markers": "python_version >= '3.7'",
"version": "==3.0.9" "version": "==0.10.2"
}, },
"flask-talisman": { "hyperframe": {
"hashes": [ "hashes": [
"sha256:468131464a249274ed226efc21b372518f442487e58918ccab8357eaa638fd1f", "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
"sha256:eaa754f4b771dfbe473843391d69643b79e3a38c865790011ac5e4179c68e3ec" "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
], ],
"index": "pypi", "version": "==5.2.0"
"version": "==0.7.0"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -219,11 +229,18 @@
}, },
"marshmallow": { "marshmallow": {
"hashes": [ "hashes": [
"sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76", "sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811",
"sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d" "sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==3.7.1" "version": "==3.8.0"
},
"priority": {
"hashes": [
"sha256:6bc1961a6d7fcacbfc337769f1a382c8e746566aaa365e78047abe9f66b2ffbe",
"sha256:be4fcb94b5e37cdeb40af5533afe6dd603bd665fe9c8b3052610fc1001d5d1eb"
],
"version": "==1.3.0"
}, },
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
@ -262,6 +279,30 @@
], ],
"version": "==0.14.0" "version": "==0.14.0"
}, },
"quart": {
"hashes": [
"sha256:9c634e4c1e4b21b824003c676de1583581258c72b0ac4d2ba747db846e97ff56",
"sha256:d885d782edd9d5dcfd2c4a56e020db3b82493d4c3950f91c221b7d88d239ac93"
],
"index": "pypi",
"version": "==0.13.1"
},
"quart-compress": {
"hashes": [
"sha256:41cd0cc8d26905a45025ddda7022461a71b9d1d950b21b006dc106a1c41c75ef",
"sha256:63af5e6370aa7850fb219d22e1db89965aeb13b8f27bc83e7f9a44118faa3c54"
],
"index": "pypi",
"version": "==0.2.1"
},
"quart-cors": {
"hashes": [
"sha256:020a17d504264db86cada3c1335ef174af28b33f57cee321ddc46d69c33d5c8e",
"sha256:c08bdb326219b6c186d19ed6a97a7fd02de8fe36c7856af889494c69b525c53c"
],
"index": "pypi",
"version": "==0.3.0"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
@ -270,6 +311,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.24.0" "version": "==2.24.0"
}, },
"secure": {
"hashes": [
"sha256:4dc8dd4b548831c3ad7f94079332c41d67c781eccc32215ff5a8a49582c1a447",
"sha256:b3bf1e39ebf40040fc3248392343a5052aa14cb45fc87ec91b0bd11f19cc46bd"
],
"index": "pypi",
"version": "==0.2.1"
},
"shortuuid": { "shortuuid": {
"hashes": [ "hashes": [
"sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f", "sha256:3c11d2007b915c43bee3e10625f068d8a349e04f0d81f08f5fa08507427ebf1f",
@ -286,13 +335,20 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0" "version": "==1.15.0"
}, },
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
},
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
], ],
"markers": "python_version < '3.8'", "index": "pypi",
"version": "==3.7.4.3" "version": "==3.7.4.3"
}, },
"urllib3": { "urllib3": {
@ -303,13 +359,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.25.10" "version": "==1.25.10"
}, },
"webassets": {
"hashes": [
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
],
"version": "==2.0"
},
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
@ -317,6 +366,14 @@
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.0.1" "version": "==1.0.1"
},
"wsproto": {
"hashes": [
"sha256:614798c30e5dc2b3f65acc03d2d50842b97621487350ce79a80a711229edfa9d",
"sha256:e3d190a11d9307112ba23bbe60055604949b172143969c8f641318476a9b6f1d"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==0.15.0"
} }
}, },
"develop": { "develop": {
@ -329,16 +386,15 @@
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
"sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.1.0" "version": "==20.2.0"
}, },
"black": { "black": {
"hashes": [ "hashes": [
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea", "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
"sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"
], ],
"index": "pypi", "index": "pypi",
"version": "==20.8b1" "version": "==20.8b1"
@ -353,67 +409,43 @@
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.2.1" "version": "==5.3"
},
"flake8": {
"hashes": [
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
"sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"
],
"index": "pypi",
"version": "==3.8.3"
},
"flake8-mypy": {
"hashes": [
"sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18",
"sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"
],
"index": "pypi",
"version": "==17.8.0"
},
"importlib-metadata": {
"hashes": [
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
],
"markers": "python_version < '3.8'",
"version": "==1.7.0"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -422,13 +454,6 @@
], ],
"version": "==1.0.1" "version": "==1.0.1"
}, },
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20", "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20",
@ -439,23 +464,23 @@
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a",
"sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7",
"sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2",
"sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474",
"sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0",
"sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217",
"sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749",
"sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6",
"sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf",
"sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36",
"sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b",
"sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72",
"sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1",
"sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"
], ],
"markers": "python_version >= '3.5'", "index": "pypi",
"version": "==0.782" "version": "==0.761"
}, },
"mypy-extensions": { "mypy-extensions": {
"hashes": [ "hashes": [
@ -495,22 +520,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.9.0" "version": "==1.9.0"
}, },
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pyflakes": {
"hashes": [
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
@ -521,11 +530,19 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4", "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40",
"sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad" "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.0.1" "version": "==6.0.2"
},
"pytest-asyncio": {
"hashes": [
"sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d",
"sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"
],
"index": "pypi",
"version": "==0.14.0"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@ -608,16 +625,8 @@
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
], ],
"markers": "python_version < '3.8'", "index": "pypi",
"version": "==3.7.4.3" "version": "==3.7.4.3"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"markers": "python_version >= '3.6'",
"version": "==3.1.0"
} }
} }
} }

2
Procfile

@ -1 +1 @@
web: gunicorn -b :5000 lnbits:app -k gevent web: hypercorn --bind 0.0.0.0:5000 lnbits:app

4
app.json

@ -1,7 +1,5 @@
{ {
"scripts": { "scripts": {
"dokku": { "dokku": {}
"predeploy": "flask migrate"
}
} }
} }

8
docs/devs/installation.md

@ -42,23 +42,19 @@ Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Ne
Running the server Running the server
------------------ ------------------
LNbits uses [Flask][flask] as an application server. LNbits uses [Quart][quart] as an application server.
```sh ```sh
$ pipenv run python -m lnbits $ pipenv run python -m lnbits
``` ```
There is an environment variable called `FLASK_ENV` that has to be set to `development`
if you want to run Flask in debug mode with autoreload
Frontend Frontend
-------- --------
The frontend uses [Vue.js and Quasar][quasar]. The frontend uses [Vue.js and Quasar][quasar].
[flask]: http://flask.pocoo.org/ [quart]: https://pgjones.gitlab.io/
[pipenv]: https://pipenv.pypa.io/ [pipenv]: https://pipenv.pypa.io/
[polar]: https://lightningpolar.com/ [polar]: https://lightningpolar.com/
[quasar]: https://quasar.dev/start/how-to-use-vue [quasar]: https://quasar.dev/start/how-to-use-vue

7
lnbits/__main__.py

@ -1,8 +1,9 @@
from .app import create_app from .app import create_app
from .commands import migrate_databases from .commands import migrate_databases, transpile_scss, bundle_vendored
migrate_databases() migrate_databases()
transpile_scss()
bundle_vendored()
app = create_app() app = create_app()
app.run() app.run(host=app.config["HOST"], port=app.config["PORT"])

92
lnbits/app.py

@ -1,27 +1,31 @@
import importlib import importlib
from flask import Flask, g from quart import Quart, g
from flask_assets import Bundle # type: ignore from quart_cors import cors # type: ignore
from flask_cors import CORS # type: ignore from quart_compress import Compress # type: ignore
from flask_talisman import Talisman # type: ignore from secure import SecureHeaders # type: ignore
from werkzeug.middleware.proxy_fix import ProxyFix
from .commands import flask_migrate from .commands import db_migrate
from .core import core_app from .core import core_app
from .db import open_db from .db import open_db
from .ext import assets, compress from .helpers import get_valid_extensions, get_js_vendored, get_css_vendored, url_for_vendored
from .helpers import get_valid_extensions from .proxy_fix import ProxyFix
secure_headers = SecureHeaders(hsts=False)
def create_app(config_object="lnbits.settings") -> Flask:
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. def create_app(config_object="lnbits.settings") -> Quart:
"""Create application factory.
:param config_object: The configuration object to use. :param config_object: The configuration object to use.
""" """
app = Flask(__name__, static_folder="static") app = Quart(__name__, static_folder="static")
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) # type: ignore
app.config.from_object(config_object) app.config.from_object(config_object)
register_flask_extensions(app) cors(app)
Compress(app)
ProxyFix(app, x_proto=1, x_host=1)
register_assets(app)
register_blueprints(app) register_blueprints(app)
register_filters(app) register_filters(app)
register_commands(app) register_commands(app)
@ -30,7 +34,7 @@ def create_app(config_object="lnbits.settings") -> Flask:
return app return app
def register_blueprints(app) -> None: def register_blueprints(app: Quart) -> None:
"""Register Flask blueprints / LNbits extensions.""" """Register Flask blueprints / LNbits extensions."""
app.register_blueprint(core_app) app.register_blueprint(core_app)
@ -42,48 +46,42 @@ def register_blueprints(app) -> None:
raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.") raise ImportError(f"Please make sure that the extension `{ext.code}` follows conventions.")
def register_commands(app): def register_commands(app: Quart):
"""Register Click commands.""" """Register Click commands."""
app.cli.add_command(flask_migrate) app.cli.add_command(db_migrate)
def register_flask_extensions(app): def register_assets(app: Quart):
"""Register Flask extensions.""" """Serve each vendored asset separately or a bundle."""
"""If possible we use the .init_app() option so that Blueprints can also use extensions."""
CORS(app) @app.before_request
Talisman( async def vendored_assets_variable():
app, if app.config["DEBUG"]:
force_https=app.config["FORCE_HTTPS"], g.VENDORED_JS = map(url_for_vendored, get_js_vendored())
content_security_policy={ g.VENDORED_CSS = map(url_for_vendored, get_css_vendored())
"default-src": [ else:
"'self'", g.VENDORED_JS = ["/static/bundle.js"]
"'unsafe-eval'", g.VENDORED_CSS = ["/static/bundle.css"]
"'unsafe-inline'",
"blob:",
"api.opennode.co", def register_filters(app: Quart):
]
},
)
assets.init_app(app)
assets.register("base_css", Bundle("scss/base.scss", filters="pyscss", output="css/base.css"))
compress.init_app(app)
def register_filters(app):
"""Jinja filters.""" """Jinja filters."""
app.jinja_env.globals["DEBUG"] = app.config["DEBUG"]
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"] app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
def register_request_hooks(app): def register_request_hooks(app: Quart):
"""Open the core db for each request so everything happens in a big transaction""" """Open the core db for each request so everything happens in a big transaction"""
@app.before_request @app.before_request
def before_request(): async def before_request():
g.db = open_db() g.db = open_db()
@app.after_request
async def set_secure_headers(response):
secure_headers.quart(response)
return response
@app.teardown_request @app.teardown_request
def after_request(exc): async def after_request(exc):
g.db.__exit__(type(exc), exc, None) g.db.__exit__(type(exc), exc, None)

33
lnbits/commands.py

@ -1,18 +1,47 @@
import click import click
import importlib import importlib
import re import re
import os
import sqlite3 import sqlite3
from scss.compiler import compile_string # type: ignore
from .core import migrations as core_migrations from .core import migrations as core_migrations
from .db import open_db, open_ext_db from .db import open_db, open_ext_db
from .helpers import get_valid_extensions from .helpers import get_valid_extensions, get_css_vendored, get_js_vendored, url_for_vendored
from .settings import LNBITS_PATH
@click.command("migrate") @click.command("migrate")
def flask_migrate(): def db_migrate():
migrate_databases() migrate_databases()
@click.command("assets")
def handle_assets():
transpile_scss()
bundle_vendored()
def transpile_scss():
with open(os.path.join(LNBITS_PATH, "static/scss/base.scss")) as scss:
with open(os.path.join(LNBITS_PATH, "static/css/base.css"), "w") as css:
css.write(compile_string(scss.read()))
def bundle_vendored():
for getfiles, outputpath in [
(get_js_vendored, os.path.join(LNBITS_PATH, "static/bundle.js")),
(get_css_vendored, os.path.join(LNBITS_PATH, "static/bundle.css")),
]:
output = ""
for path in getfiles():
with open(path) as f:
output += "/* " + url_for_vendored(path) + " */\n" + f.read() + ";\n"
with open(outputpath, "w") as f:
f.write(output)
def migrate_databases(): def migrate_databases():
"""Creates the necessary databases if they don't exist already; or migrates them.""" """Creates the necessary databases if they don't exist already; or migrates them."""

6
lnbits/core/__init__.py

@ -1,7 +1,9 @@
from flask import Blueprint from quart import Blueprint
core_app: Blueprint = Blueprint("core", __name__, template_folder="templates", static_folder="static") core_app: Blueprint = Blueprint(
"core", __name__, template_folder="templates", static_folder="static", static_url_path="/core/static"
)
from .views.api import * # noqa from .views.api import * # noqa

2
lnbits/core/crud.py

@ -2,7 +2,7 @@ import json
import datetime import datetime
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, Dict from typing import List, Optional, Dict
from flask import g from quart import g
from lnbits import bolt11 from lnbits import bolt11
from lnbits.settings import DEFAULT_WALLET_NAME from lnbits.settings import DEFAULT_WALLET_NAME

2
lnbits/core/services.py

@ -1,5 +1,5 @@
from typing import Optional, Tuple, Dict from typing import Optional, Tuple, Dict
from flask import g from quart import g
try: try:
from typing import TypedDict # type: ignore from typing import TypedDict # type: ignore

7
lnbits/core/templates/core/extensions.html

@ -1,8 +1,7 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }} {% assets filters='rjsmin', %} {% block scripts %} {{ window_vars(user) }}
output='__bundle__/core/extensions.js', 'core/js/extensions.js' %} <script src="/core/static/js/extensions.js"></script>
<script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endblock %} {% block page %}
{% endassets %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div <div
class="col-6 col-md-4 col-lg-3" class="col-6 col-md-4 col-lg-3"

7
lnbits/core/templates/core/index.html

@ -1,7 +1,6 @@
{% extends "public.html" %} {% block scripts %} {% assets filters='rjsmin', {% extends "public.html" %} {% block scripts %}
output='__bundle__/core/index.js', 'core/js/index.js' %} <script src="/core/static/js/index.js"></script>
<script type="text/javascript" src="{{ ASSET_URL }}"></script> {% endblock %} {% block page %}
{% endassets %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md justify-center"> <div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md"> <div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card> <q-card>

20
lnbits/core/templates/core/wallet.html

@ -1,21 +1,7 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block styles %} %} {% block scripts %} {{ window_vars(user, wallet) }}
<link <script src="/core/static/js/wallet.js"></script>
rel="stylesheet" {% endblock %} {% block page %}
type="text/css"
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
/>
{% endblock %} {% block scripts %} {{ window_vars(user, wallet) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js', 'core/js/wallet.js'
%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md"> <div class="col-12 col-md-7 q-gutter-y-md">
<q-card> <q-card>

16
lnbits/core/views/api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from binascii import unhexlify from binascii import unhexlify
@ -12,7 +12,7 @@ from lnbits.settings import WALLET
@core_app.route("/api/v1/payments", methods=["GET"]) @core_app.route("/api/v1/payments", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_payments(): async def api_payments():
if "check_pending" in request.args: if "check_pending" in request.args:
delete_expired_invoices() delete_expired_invoices()
@ -33,7 +33,7 @@ def api_payments():
"description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"}, "description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"},
} }
) )
def api_payments_create_invoice(): async def api_payments_create_invoice():
if "description_hash" in g.data: if "description_hash" in g.data:
description_hash = unhexlify(g.data["description_hash"]) description_hash = unhexlify(g.data["description_hash"])
memo = "" memo = ""
@ -65,7 +65,7 @@ def api_payments_create_invoice():
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
@api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"bolt11": {"type": "string", "empty": False, "required": True}})
def api_payments_pay_invoice(): async def api_payments_pay_invoice():
try: try:
payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"]) payment_hash = pay_invoice(wallet_id=g.wallet.id, payment_request=g.data["bolt11"])
except ValueError as e: except ValueError as e:
@ -91,15 +91,15 @@ def api_payments_pay_invoice():
@core_app.route("/api/v1/payments", methods=["POST"]) @core_app.route("/api/v1/payments", methods=["POST"])
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}}) @api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
def api_payments_create(): async def api_payments_create():
if g.data["out"] is True: if g.data["out"] is True:
return api_payments_pay_invoice() return await api_payments_pay_invoice()
return api_payments_create_invoice() return await api_payments_create_invoice()
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"]) @core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_payment(payment_hash): async def api_payment(payment_hash):
payment = g.wallet.get_payment(payment_hash) payment = g.wallet.get_payment(payment_hash)
if not payment: if not payment:

22
lnbits/core/views/generic.py

@ -1,4 +1,4 @@
from flask import g, abort, redirect, request, render_template, send_from_directory, url_for from quart import g, abort, redirect, request, render_template, send_from_directory, url_for
from http import HTTPStatus from http import HTTPStatus
from os import path from os import path
@ -16,19 +16,19 @@ from ..crud import (
@core_app.route("/favicon.ico") @core_app.route("/favicon.ico")
def favicon(): async def favicon():
return send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico") return await send_from_directory(path.join(core_app.root_path, "static"), "favicon.ico")
@core_app.route("/") @core_app.route("/")
def home(): async def home():
return render_template("core/index.html", lnurl=request.args.get("lightning", None)) return await render_template("core/index.html", lnurl=request.args.get("lightning", None))
@core_app.route("/extensions") @core_app.route("/extensions")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def extensions(): async def extensions():
extension_to_enable = request.args.get("enable", type=str) extension_to_enable = request.args.get("enable", type=str)
extension_to_disable = request.args.get("disable", type=str) extension_to_disable = request.args.get("disable", type=str)
@ -40,12 +40,12 @@ def extensions():
elif extension_to_disable: elif extension_to_disable:
update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0) update_user_extension(user_id=g.user.id, extension=extension_to_disable, active=0)
return render_template("core/extensions.html", user=get_user(g.user.id)) return await render_template("core/extensions.html", user=get_user(g.user.id))
@core_app.route("/wallet") @core_app.route("/wallet")
@validate_uuids(["usr", "wal"]) @validate_uuids(["usr", "wal"])
def wallet(): async def wallet():
user_id = request.args.get("usr", type=str) user_id = request.args.get("usr", type=str)
wallet_id = request.args.get("wal", type=str) wallet_id = request.args.get("wal", type=str)
wallet_name = request.args.get("nme", type=str) wallet_name = request.args.get("nme", type=str)
@ -76,13 +76,15 @@ def wallet():
if wallet_id not in user.wallet_ids: if wallet_id not in user.wallet_ids:
abort(HTTPStatus.FORBIDDEN, "Not your wallet.") abort(HTTPStatus.FORBIDDEN, "Not your wallet.")
return render_template("core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee) return await render_template(
"core/wallet.html", user=user, wallet=user.get_wallet(wallet_id), service_fee=service_fee
)
@core_app.route("/deletewallet") @core_app.route("/deletewallet")
@validate_uuids(["usr", "wal"], required=True) @validate_uuids(["usr", "wal"], required=True)
@check_user_exists() @check_user_exists()
def deletewallet(): async def deletewallet():
wallet_id = request.args.get("wal", type=str) wallet_id = request.args.get("wal", type=str)
user_wallet_ids = g.user.wallet_ids user_wallet_ids = g.user.wallet_ids

4
lnbits/core/views/lnurl.py

@ -1,6 +1,6 @@
import requests import requests
from flask import abort, redirect, request, url_for from quart import abort, redirect, request, url_for
from http import HTTPStatus from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore
from lnurl.exceptions import LnurlException # type: ignore from lnurl.exceptions import LnurlException # type: ignore
@ -13,7 +13,7 @@ from ..crud import create_account, get_user, create_wallet, create_payment
@core_app.route("/lnurlwallet") @core_app.route("/lnurlwallet")
def lnurlwallet(): async def lnurlwallet():
memo = "LNbits LNURL funding" memo = "LNbits LNURL funding"
try: try:

21
lnbits/decorators.py

@ -1,5 +1,5 @@
from cerberus import Validator # type: ignore from cerberus import Validator # type: ignore
from flask import g, abort, jsonify, request from quart import g, abort, jsonify, request
from functools import wraps from functools import wraps
from http import HTTPStatus from http import HTTPStatus
from typing import List, Union from typing import List, Union
@ -12,7 +12,7 @@ from lnbits.settings import LNBITS_ALLOWED_USERS
def api_check_wallet_key(key_type: str = "invoice"): def api_check_wallet_key(key_type: str = "invoice"):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
try: try:
g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type) g.wallet = get_wallet_for_key(request.headers["X-Api-Key"], key_type)
except KeyError: except KeyError:
@ -24,7 +24,7 @@ def api_check_wallet_key(key_type: str = "invoice"):
if not g.wallet: if not g.wallet:
return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED return jsonify({"message": "Wrong keys."}), HTTPStatus.UNAUTHORIZED
return view(**kwargs) return await view(**kwargs)
return wrapped_view return wrapped_view
@ -34,7 +34,7 @@ def api_check_wallet_key(key_type: str = "invoice"):
def api_validate_post_request(*, schema: dict): def api_validate_post_request(*, schema: dict):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
if "application/json" not in request.headers["Content-Type"]: if "application/json" not in request.headers["Content-Type"]:
return ( return (
jsonify({"message": "Content-Type must be `application/json`."}), jsonify({"message": "Content-Type must be `application/json`."}),
@ -42,7 +42,8 @@ def api_validate_post_request(*, schema: dict):
) )
v = Validator(schema) v = Validator(schema)
g.data = {key: request.json[key] for key in schema.keys() if key in request.json} 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): if not v.validate(g.data):
return ( return (
@ -50,7 +51,7 @@ def api_validate_post_request(*, schema: dict):
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
return view(**kwargs) return await view(**kwargs)
return wrapped_view return wrapped_view
@ -60,13 +61,13 @@ def api_validate_post_request(*, schema: dict):
def check_user_exists(param: str = "usr"): def check_user_exists(param: str = "usr"):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.") g.user = get_user(request.args.get(param, type=str)) or abort(HTTPStatus.NOT_FOUND, "User does not exist.")
if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS: if LNBITS_ALLOWED_USERS and g.user.id not in LNBITS_ALLOWED_USERS:
abort(HTTPStatus.UNAUTHORIZED, "User not authorized.") abort(HTTPStatus.UNAUTHORIZED, "User not authorized.")
return view(**kwargs) return await view(**kwargs)
return wrapped_view return wrapped_view
@ -76,7 +77,7 @@ def check_user_exists(param: str = "usr"):
def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4): def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = False, version: int = 4):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
query_params = {param: request.args.get(param, type=str) for param in params} query_params = {param: request.args.get(param, type=str) for param in params}
for param, value in query_params.items(): for param, value in query_params.items():
@ -89,7 +90,7 @@ def validate_uuids(params: List[str], *, required: Union[bool, List[str]] = Fals
except ValueError: except ValueError:
abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.") abort(HTTPStatus.BAD_REQUEST, f"`{param}` is not a valid UUID.")
return view(**kwargs) return await view(**kwargs)
return wrapped_view return wrapped_view

6
lnbits/ext.py

@ -1,6 +0,0 @@
from flask_assets import Environment # type: ignore
from flask_compress import Compress # type: ignore
assets = Environment()
compress = Compress()

2
lnbits/extensions/amilk/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates") amilk_ext: Blueprint = Blueprint("amilk", __name__, static_folder="static", template_folder="templates")

11
lnbits/extensions/amilk/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -10,12 +10,11 @@ from .crud import get_amilk
@amilk_ext.route("/") @amilk_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("amilk/index.html", user=g.user) return await render_template("amilk/index.html", user=g.user)
@amilk_ext.route("/<amilk_id>") @amilk_ext.route("/<amilk_id>")
def wall(amilk_id): async def wall(amilk_id):
amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.") amilk = get_amilk(amilk_id) or abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.")
return await render_template("amilk/wall.html", amilk=amilk)
return render_template("amilk/wall.html", amilk=amilk)

10
lnbits/extensions/amilk/views_api.py

@ -1,5 +1,5 @@
import requests import requests
from flask import g, jsonify, request, abort from quart import g, jsonify, request, abort
from http import HTTPStatus from http import HTTPStatus
from lnurl import LnurlWithdrawResponse, handle as handle_lnurl from lnurl import LnurlWithdrawResponse, handle as handle_lnurl
from lnurl.exceptions import LnurlException from lnurl.exceptions import LnurlException
@ -15,7 +15,7 @@ from .crud import create_amilk, get_amilk, get_amilks, delete_amilk
@amilk_ext.route("/api/v1/amilk", methods=["GET"]) @amilk_ext.route("/api/v1/amilk", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_amilks(): async def api_amilks():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -25,7 +25,7 @@ def api_amilks():
@amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"]) @amilk_ext.route("/api/v1/amilk/milk/<amilk_id>", methods=["GET"])
def api_amilkit(amilk_id): async def api_amilkit(amilk_id):
milk = get_amilk(amilk_id) milk = get_amilk(amilk_id)
memo = milk.id memo = milk.id
@ -66,7 +66,7 @@ def api_amilkit(amilk_id):
"amount": {"type": "integer", "min": 0, "required": True}, "amount": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_amilk_create(): async def api_amilk_create():
amilk = create_amilk(wallet_id=g.wallet.id, lnurl=g.data["lnurl"], atime=g.data["atime"], amount=g.data["amount"]) amilk = 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 return jsonify(amilk._asdict()), HTTPStatus.CREATED
@ -74,7 +74,7 @@ def api_amilk_create():
@amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"]) @amilk_ext.route("/api/v1/amilk/<amilk_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_amilk_delete(amilk_id): async def api_amilk_delete(amilk_id):
amilk = get_amilk(amilk_id) amilk = get_amilk(amilk_id)
if not amilk: if not amilk:

2
lnbits/extensions/diagonalley/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates") diagonalley_ext: Blueprint = Blueprint("diagonalley", __name__, static_folder="static", template_folder="templates")

10
lnbits/extensions/diagonalley/views.py

@ -1,15 +1,11 @@
import json from quart import g, render_template
from flask import g, abort, render_template, jsonify
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.diagonalley import diagonalley_ext from lnbits.extensions.diagonalley import diagonalley_ext
from lnbits.db import open_ext_db
@diagonalley_ext.route("/") @diagonalley_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return await render_template("diagonalley/index.html", user=g.user)
return render_template("diagonalley/index.html", user=g.user)

32
lnbits/extensions/diagonalley/views_api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
@ -18,7 +18,7 @@ from .crud import (
create_diagonalleys_order, create_diagonalleys_order,
get_diagonalleys_order, get_diagonalleys_order,
get_diagonalleys_orders, get_diagonalleys_orders,
delete_diagonalleys_order, update_diagonalleys_product,
) )
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
@ -30,7 +30,7 @@ from lnbits.db import open_ext_db
@diagonalley_ext.route("/api/v1/diagonalley/products", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/products", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_products(): async def api_diagonalley_products():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -52,7 +52,7 @@ def api_diagonalley_products():
"quantity": {"type": "integer", "min": 0, "required": True}, "quantity": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_diagonalley_product_create(product_id=None): async def api_diagonalley_product_create(product_id=None):
if product_id: if product_id:
product = get_diagonalleys_indexer(product_id) product = get_diagonalleys_indexer(product_id)
@ -72,7 +72,7 @@ def api_diagonalley_product_create(product_id=None):
@diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"]) @diagonalley_ext.route("/api/v1/diagonalley/products/<product_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_products_delete(product_id): async def api_diagonalley_products_delete(product_id):
product = get_diagonalleys_product(product_id) product = get_diagonalleys_product(product_id)
if not product: if not product:
@ -91,7 +91,7 @@ def api_diagonalley_products_delete(product_id):
@diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/indexers", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_indexers(): async def api_diagonalley_indexers():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -114,7 +114,7 @@ def api_diagonalley_indexers():
"zone2cost": {"type": "integer", "min": 0, "required": True}, "zone2cost": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_diagonalley_indexer_create(indexer_id=None): async def api_diagonalley_indexer_create(indexer_id=None):
if indexer_id: if indexer_id:
indexer = get_diagonalleys_indexer(indexer_id) indexer = get_diagonalleys_indexer(indexer_id)
@ -134,7 +134,7 @@ def api_diagonalley_indexer_create(indexer_id=None):
@diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"]) @diagonalley_ext.route("/api/v1/diagonalley/indexers/<indexer_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_indexer_delete(indexer_id): async def api_diagonalley_indexer_delete(indexer_id):
indexer = get_diagonalleys_indexer(indexer_id) indexer = get_diagonalleys_indexer(indexer_id)
if not indexer: if not indexer:
@ -153,7 +153,7 @@ def api_diagonalley_indexer_delete(indexer_id):
@diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/orders", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_orders(): async def api_diagonalley_orders():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -173,14 +173,14 @@ def api_diagonalley_orders():
"shippingzone": {"type": "integer", "empty": False, "required": True}, "shippingzone": {"type": "integer", "empty": False, "required": True},
} }
) )
def api_diagonalley_order_create(): async def api_diagonalley_order_create():
order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data) order = create_diagonalleys_order(wallet_id=g.wallet.id, **g.data)
return jsonify(order._asdict()), HTTPStatus.CREATED return jsonify(order._asdict()), HTTPStatus.CREATED
@diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"]) @diagonalley_ext.route("/api/v1/diagonalley/orders/<order_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalley_order_delete(order_id): async def api_diagonalley_order_delete(order_id):
order = get_diagonalleys_order(order_id) order = get_diagonalleys_order(order_id)
if not order: if not order:
@ -196,7 +196,7 @@ def api_diagonalley_order_delete(order_id):
@diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/orders/paid/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalleys_order_paid(order_id): async def api_diagonalleys_order_paid(order_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
db.execute( db.execute(
"UPDATE orders SET paid = ? WHERE id = ?", "UPDATE orders SET paid = ? WHERE id = ?",
@ -210,7 +210,7 @@ def api_diagonalleys_order_paid(order_id):
@diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/orders/shipped/<order_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_diagonalleys_order_shipped(order_id): async def api_diagonalleys_order_shipped(order_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
db.execute( db.execute(
"UPDATE orders SET shipped = ? WHERE id = ?", "UPDATE orders SET shipped = ? WHERE id = ?",
@ -228,7 +228,7 @@ def api_diagonalleys_order_shipped(order_id):
@diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/stall/products/<indexer_id>", methods=["GET"])
def api_diagonalleys_stall_products(indexer_id): async def api_diagonalleys_stall_products(indexer_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,)) rows = db.fetchone("SELECT * FROM indexers WHERE id = ?", (indexer_id,))
print(rows[1]) print(rows[1])
@ -246,7 +246,7 @@ def api_diagonalleys_stall_products(indexer_id):
@diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"]) @diagonalley_ext.route("/api/v1/diagonalley/stall/checkshipped/<checking_id>", methods=["GET"])
def api_diagonalleys_stall_checkshipped(checking_id): async def api_diagonalleys_stall_checkshipped(checking_id):
with open_ext_db("diagonalley") as db: with open_ext_db("diagonalley") as db:
rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,)) rows = db.fetchone("SELECT * FROM orders WHERE invoiceid = ?", (checking_id,))
@ -266,7 +266,7 @@ def api_diagonalleys_stall_checkshipped(checking_id):
"shippingzone": {"type": "integer", "empty": False, "required": True}, "shippingzone": {"type": "integer", "empty": False, "required": True},
} }
) )
def api_diagonalley_stall_order(indexer_id): async def api_diagonalley_stall_order(indexer_id):
product = get_diagonalleys_product(g.data["id"]) product = get_diagonalleys_product(g.data["id"])
shipping = get_diagonalleys_indexer(indexer_id) shipping = get_diagonalleys_indexer(indexer_id)

2
lnbits/extensions/events/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates") events_ext: Blueprint = Blueprint("events", __name__, static_folder="static", template_folder="templates")

1
lnbits/extensions/events/templates/events/display.html

@ -82,7 +82,6 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
console.log('{{ form_costpword }}') console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

17
lnbits/extensions/events/templates/events/register.html

@ -78,24 +78,7 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
</div> </div>
{% endblock %} {% block styles %}
<link
rel="stylesheet"
type="text/css"
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
/>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
'vendor/moment@2.27.0/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
Vue.use(VueQrcodeReader) Vue.use(VueQrcodeReader)

1
lnbits/extensions/events/templates/events/ticket.html

@ -27,7 +27,6 @@
</div> </div>
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
new Vue({ new Vue({

28
lnbits/extensions/events/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from datetime import date, datetime from datetime import date, datetime
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -11,22 +11,24 @@ from .crud import get_ticket, get_event
@events_ext.route("/") @events_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("events/index.html", user=g.user) return await render_template("events/index.html", user=g.user)
@events_ext.route("/<event_id>") @events_ext.route("/<event_id>")
def display(event_id): async def display(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
if event.amount_tickets < 1: if event.amount_tickets < 1:
return render_template("events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :(") return await render_template(
"events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :("
)
datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date() datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date()
if date.today() > datetime_object: if date.today() > datetime_object:
return render_template( return await render_template(
"events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(" "events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :("
) )
return render_template( return await render_template(
"events/display.html", "events/display.html",
event_id=event_id, event_id=event_id,
event_name=event.name, event_name=event.name,
@ -36,14 +38,18 @@ def display(event_id):
@events_ext.route("/ticket/<ticket_id>") @events_ext.route("/ticket/<ticket_id>")
def ticket(ticket_id): async def ticket(ticket_id):
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.") ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return render_template("events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info) return await render_template(
"events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info
)
@events_ext.route("/register/<event_id>") @events_ext.route("/register/<event_id>")
def register(event_id): async def register(event_id):
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.") event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
return render_template("events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet) return await render_template(
"events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet
)

20
lnbits/extensions/events/views_api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
@ -27,7 +27,7 @@ from .crud import (
@events_ext.route("/api/v1/events", methods=["GET"]) @events_ext.route("/api/v1/events", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_events(): async def api_events():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -51,7 +51,7 @@ def api_events():
"price_per_ticket": {"type": "integer", "min": 0, "required": True}, "price_per_ticket": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_event_create(event_id=None): async def api_event_create(event_id=None):
if event_id: if event_id:
event = get_event(event_id) event = get_event(event_id)
print(g.data) print(g.data)
@ -71,7 +71,7 @@ def api_event_create(event_id=None):
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"]) @events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_form_delete(event_id): async def api_form_delete(event_id):
event = get_event(event_id) event = get_event(event_id)
if not event: if not event:
@ -90,7 +90,7 @@ def api_form_delete(event_id):
@events_ext.route("/api/v1/tickets", methods=["GET"]) @events_ext.route("/api/v1/tickets", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_tickets(): async def api_tickets():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -106,7 +106,7 @@ def api_tickets():
"email": {"type": "string", "empty": False, "required": True}, "email": {"type": "string", "empty": False, "required": True},
} }
) )
def api_ticket_make_ticket(event_id, sats): async def api_ticket_make_ticket(event_id, sats):
event = get_event(event_id) event = get_event(event_id)
if not event: if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
@ -126,7 +126,7 @@ def api_ticket_make_ticket(event_id, sats):
@events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
def api_ticket_send_ticket(payment_hash): async def api_ticket_send_ticket(payment_hash):
ticket = get_ticket(payment_hash) ticket = get_ticket(payment_hash)
try: try:
is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending
@ -146,7 +146,7 @@ def api_ticket_send_ticket(payment_hash):
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"]) @events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_ticket_delete(ticket_id): async def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id) ticket = get_ticket(ticket_id)
if not ticket: if not ticket:
@ -164,7 +164,7 @@ def api_ticket_delete(ticket_id):
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"]) @events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
def api_event_tickets(wallet_id, event_id): async def api_event_tickets(wallet_id, event_id):
return ( return (
jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]),
@ -173,7 +173,7 @@ def api_event_tickets(wallet_id, event_id):
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"]) @events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
def api_event_register_ticket(ticket_id): async def api_event_register_ticket(ticket_id):
ticket = get_ticket(ticket_id) ticket = get_ticket(ticket_id)

2
lnbits/extensions/example/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates") example_ext: Blueprint = Blueprint("example", __name__, static_folder="static", template_folder="templates")

6
lnbits/extensions/example/views.py

@ -1,4 +1,4 @@
from flask import g, render_template from quart import g, render_template
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.example import example_ext from lnbits.extensions.example import example_ext
@ -7,5 +7,5 @@ from lnbits.extensions.example import example_ext
@example_ext.route("/") @example_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("example/index.html", user=g.user) return await render_template("example/index.html", user=g.user)

4
lnbits/extensions/example/views_api.py

@ -5,7 +5,7 @@
# import json # import json
# import requests # import requests
from flask import jsonify from quart import jsonify
from http import HTTPStatus from http import HTTPStatus
from lnbits.extensions.example import example_ext from lnbits.extensions.example import example_ext
@ -15,7 +15,7 @@ from lnbits.extensions.example import example_ext
@example_ext.route("/api/v1/tools", methods=["GET"]) @example_ext.route("/api/v1/tools", methods=["GET"])
def api_example(): async def api_example():
"""Try to add descriptions for others.""" """Try to add descriptions for others."""
tools = [ tools = [
{ {

2
lnbits/extensions/lndhub/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates") lndhub_ext: Blueprint = Blueprint("lndhub", __name__, static_folder="static", template_folder="templates")

6
lnbits/extensions/lndhub/decorators.py

@ -1,5 +1,5 @@
from base64 import b64decode from base64 import b64decode
from flask import jsonify, g, request from quart import jsonify, g, request
from functools import wraps from functools import wraps
from lnbits.core.crud import get_wallet_for_key from lnbits.core.crud import get_wallet_for_key
@ -8,7 +8,7 @@ from lnbits.core.crud import get_wallet_for_key
def check_wallet(requires_admin=False): def check_wallet(requires_admin=False):
def wrap(view): def wrap(view):
@wraps(view) @wraps(view)
def wrapped_view(**kwargs): async def wrapped_view(**kwargs):
token = request.headers["Authorization"].split("Bearer ")[1] token = request.headers["Authorization"].split("Bearer ")[1]
key_type, key = b64decode(token).decode("utf-8").split(":") key_type, key = b64decode(token).decode("utf-8").split(":")
@ -18,7 +18,7 @@ def check_wallet(requires_admin=False):
g.wallet = get_wallet_for_key(key, key_type) g.wallet = get_wallet_for_key(key, key_type)
if not g.wallet: if not g.wallet:
return jsonify({"error": True, "code": 2, "message": "insufficient permissions"}) return jsonify({"error": True, "code": 2, "message": "insufficient permissions"})
return view(**kwargs) return await view(**kwargs)
return wrapped_view return wrapped_view

7
lnbits/extensions/lndhub/templates/lndhub/index.html

@ -67,7 +67,6 @@
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)
@ -75,7 +74,9 @@
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
var wallets = ({{ g.user.wallets | tojson }}).map(LNbits.map.wallet).map(wallet => ({ var wallets = JSON.parse('{{ g.user.wallets | tojson }}')
.map(LNbits.map.wallet)
.map(wallet => ({
label: wallet.name, label: wallet.name,
admin: `lndhub://admin:${wallet.adminkey}@${location.protocol}//${location.host}/lndhub/ext/`, admin: `lndhub://admin:${wallet.adminkey}@${location.protocol}//${location.host}/lndhub/ext/`,
invoice: `lndhub://invoice:${wallet.inkey}@${location.protocol}//${location.host}/lndhub/ext/` invoice: `lndhub://invoice:${wallet.inkey}@${location.protocol}//${location.host}/lndhub/ext/`
@ -85,7 +86,7 @@
wallets: wallets, wallets: wallets,
selectedWallet: wallets[0] selectedWallet: wallets[0]
} }
}, }
}) })
</script> </script>
{% endblock %} {% endblock %}

6
lnbits/extensions/lndhub/views.py

@ -1,4 +1,4 @@
from flask import render_template, g from quart import render_template, g
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.lndhub import lndhub_ext from lnbits.extensions.lndhub import lndhub_ext
@ -7,5 +7,5 @@ from lnbits.extensions.lndhub import lndhub_ext
@lndhub_ext.route("/") @lndhub_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def lndhub_index(): async def lndhub_index():
return render_template("lndhub/index.html", user=g.user) return await render_template("lndhub/index.html", user=g.user)

24
lnbits/extensions/lndhub/views_api.py

@ -1,6 +1,6 @@
import time import time
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from flask import jsonify, g, request from quart import jsonify, g, request
from lnbits.core.services import pay_invoice, create_invoice from lnbits.core.services import pay_invoice, create_invoice
from lnbits.core.crud import delete_expired_invoices from lnbits.core.crud import delete_expired_invoices
@ -14,7 +14,7 @@ from .utils import to_buffer, decoded_as_lndhub
@lndhub_ext.route("/ext/getinfo", methods=["GET"]) @lndhub_ext.route("/ext/getinfo", methods=["GET"])
def lndhub_getinfo(): async def lndhub_getinfo():
return jsonify({"error": True, "code": 1, "message": "bad auth"}) return jsonify({"error": True, "code": 1, "message": "bad auth"})
@ -26,7 +26,7 @@ def lndhub_getinfo():
"refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]}, "refresh_token": {"type": "string", "required": True, "excludes": ["login", "password"]},
} }
) )
def lndhub_auth(): async def lndhub_auth():
token = ( token = (
g.data["token"] g.data["token"]
if "token" in g.data and g.data["token"] if "token" in g.data and g.data["token"]
@ -44,7 +44,7 @@ def lndhub_auth():
"preimage": {"type": "string", "required": False}, "preimage": {"type": "string", "required": False},
} }
) )
def lndhub_addinvoice(): async def lndhub_addinvoice():
try: try:
_, pr = create_invoice( _, pr = create_invoice(
wallet_id=g.wallet.id, wallet_id=g.wallet.id,
@ -76,7 +76,7 @@ def lndhub_addinvoice():
@lndhub_ext.route("/ext/payinvoice", methods=["POST"]) @lndhub_ext.route("/ext/payinvoice", methods=["POST"])
@check_wallet(requires_admin=True) @check_wallet(requires_admin=True)
@api_validate_post_request(schema={"invoice": {"type": "string", "required": True}}) @api_validate_post_request(schema={"invoice": {"type": "string", "required": True}})
def lndhub_payinvoice(): async def lndhub_payinvoice():
try: try:
pay_invoice( pay_invoice(
wallet_id=g.wallet.id, wallet_id=g.wallet.id,
@ -112,13 +112,13 @@ def lndhub_payinvoice():
@lndhub_ext.route("/ext/balance", methods=["GET"]) @lndhub_ext.route("/ext/balance", methods=["GET"])
@check_wallet() @check_wallet()
def lndhub_balance(): async def lndhub_balance():
return jsonify({"BTC": {"AvailableBalance": g.wallet.balance}}) return jsonify({"BTC": {"AvailableBalance": g.wallet.balance}})
@lndhub_ext.route("/ext/gettxs", methods=["GET"]) @lndhub_ext.route("/ext/gettxs", methods=["GET"])
@check_wallet() @check_wallet()
def lndhub_gettxs(): async def lndhub_gettxs():
for payment in g.wallet.get_payments( for payment in g.wallet.get_payments(
complete=False, pending=True, outgoing=True, incoming=False, exclude_uncheckable=True complete=False, pending=True, outgoing=True, incoming=False, exclude_uncheckable=True
): ):
@ -146,7 +146,7 @@ def lndhub_gettxs():
@lndhub_ext.route("/ext/getuserinvoices", methods=["GET"]) @lndhub_ext.route("/ext/getuserinvoices", methods=["GET"])
@check_wallet() @check_wallet()
def lndhub_getuserinvoices(): async def lndhub_getuserinvoices():
delete_expired_invoices() delete_expired_invoices()
for invoice in g.wallet.get_payments( for invoice in g.wallet.get_payments(
complete=False, pending=True, outgoing=False, incoming=True, exclude_uncheckable=True complete=False, pending=True, outgoing=False, incoming=True, exclude_uncheckable=True
@ -177,26 +177,26 @@ def lndhub_getuserinvoices():
@lndhub_ext.route("/ext/getbtc", methods=["GET"]) @lndhub_ext.route("/ext/getbtc", methods=["GET"])
@check_wallet() @check_wallet()
def lndhub_getbtc(): async def lndhub_getbtc():
"load an address for incoming onchain btc" "load an address for incoming onchain btc"
return jsonify([]) return jsonify([])
@lndhub_ext.route("/ext/getpending", methods=["GET"]) @lndhub_ext.route("/ext/getpending", methods=["GET"])
@check_wallet() @check_wallet()
def lndhub_getpending(): async def lndhub_getpending():
"pending onchain transactions" "pending onchain transactions"
return jsonify([]) return jsonify([])
@lndhub_ext.route("/ext/decodeinvoice", methods=["GET"]) @lndhub_ext.route("/ext/decodeinvoice", methods=["GET"])
def lndhub_decodeinvoice(): async def lndhub_decodeinvoice():
invoice = request.args.get("invoice") invoice = request.args.get("invoice")
inv = bolt11.decode(invoice) inv = bolt11.decode(invoice)
return jsonify(decoded_as_lndhub(inv)) return jsonify(decoded_as_lndhub(inv))
@lndhub_ext.route("/ext/checkrouteinvoice", methods=["GET"]) @lndhub_ext.route("/ext/checkrouteinvoice", methods=["GET"])
def lndhub_checkrouteinvoice(): async def lndhub_checkrouteinvoice():
"not implemented on canonical lndhub" "not implemented on canonical lndhub"
pass pass

2
lnbits/extensions/lnticket/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates") lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates")

1
lnbits/extensions/lnticket/templates/lnticket/display.html

@ -76,7 +76,6 @@
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
console.log('{{ form_costpword }}') console.log('{{ form_costpword }}')
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

10
lnbits/extensions/lnticket/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from http import HTTPStatus from http import HTTPStatus
@ -10,16 +10,16 @@ from .crud import get_form
@lnticket_ext.route("/") @lnticket_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("lnticket/index.html", user=g.user) return await render_template("lnticket/index.html", user=g.user)
@lnticket_ext.route("/<form_id>") @lnticket_ext.route("/<form_id>")
def display(form_id): async def display(form_id):
form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.") form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.")
print(form.id) print(form.id)
return render_template( return await render_template(
"lnticket/display.html", "lnticket/display.html",
form_id=form.id, form_id=form.id,
form_name=form.name, form_name=form.name,

16
lnbits/extensions/lnticket/views_api.py

@ -1,5 +1,5 @@
import re import re
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
@ -26,7 +26,7 @@ from .crud import (
@lnticket_ext.route("/api/v1/forms", methods=["GET"]) @lnticket_ext.route("/api/v1/forms", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_forms(): async def api_forms():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -46,7 +46,7 @@ def api_forms():
"costpword": {"type": "integer", "min": 0, "required": True}, "costpword": {"type": "integer", "min": 0, "required": True},
} }
) )
def api_form_create(form_id=None): async def api_form_create(form_id=None):
if form_id: if form_id:
form = get_form(form_id) form = get_form(form_id)
@ -64,7 +64,7 @@ def api_form_create(form_id=None):
@lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"]) @lnticket_ext.route("/api/v1/forms/<form_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_form_delete(form_id): async def api_form_delete(form_id):
form = get_form(form_id) form = get_form(form_id)
if not form: if not form:
@ -83,7 +83,7 @@ def api_form_delete(form_id):
@lnticket_ext.route("/api/v1/tickets", methods=["GET"]) @lnticket_ext.route("/api/v1/tickets", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_tickets(): async def api_tickets():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -101,7 +101,7 @@ def api_tickets():
"ltext": {"type": "string", "empty": False, "required": True}, "ltext": {"type": "string", "empty": False, "required": True},
} }
) )
def api_ticket_make_ticket(form_id): async def api_ticket_make_ticket(form_id):
form = get_form(form_id) form = get_form(form_id)
if not form: if not form:
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
@ -126,7 +126,7 @@ def api_ticket_make_ticket(form_id):
@lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"]) @lnticket_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
def api_ticket_send_ticket(payment_hash): async def api_ticket_send_ticket(payment_hash):
ticket = get_ticket(payment_hash) ticket = get_ticket(payment_hash)
try: try:
is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending is_paid = not check_invoice_status(ticket.wallet, payment_hash).pending
@ -145,7 +145,7 @@ def api_ticket_send_ticket(payment_hash):
@lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"]) @lnticket_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_ticket_delete(ticket_id): async def api_ticket_delete(ticket_id):
ticket = get_ticket(ticket_id) ticket = get_ticket(ticket_id)
if not ticket: if not ticket:

2
lnbits/extensions/lnurlp/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates") lnurlp_ext: Blueprint = Blueprint("lnurlp", __name__, static_folder="static", template_folder="templates")

2
lnbits/extensions/lnurlp/models.py

@ -1,5 +1,5 @@
import json import json
from flask import url_for from quart import url_for
from lnurl import Lnurl, encode as lnurl_encode from lnurl import Lnurl, encode as lnurl_encode
from lnurl.types import LnurlPayMetadata from lnurl.types import LnurlPayMetadata
from sqlite3 import Row from sqlite3 import Row

1
lnbits/extensions/lnurlp/templates/lnurlp/display.html

@ -38,7 +38,6 @@
</div> </div>
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

1
lnbits/extensions/lnurlp/templates/lnurlp/index.html

@ -205,7 +205,6 @@
</q-dialog> </q-dialog>
</div> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

1
lnbits/extensions/lnurlp/templates/lnurlp/print_qr.html

@ -11,7 +11,6 @@
} }
</style> </style>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

16
lnbits/extensions/lnurlp/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -10,19 +10,17 @@ from .crud import get_pay_link
@lnurlp_ext.route("/") @lnurlp_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("lnurlp/index.html", user=g.user) return await render_template("lnurlp/index.html", user=g.user)
@lnurlp_ext.route("/<link_id>") @lnurlp_ext.route("/<link_id>")
def display(link_id): async def display(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return await render_template("lnurlp/display.html", link=link)
return render_template("lnurlp/display.html", link=link)
@lnurlp_ext.route("/print/<link_id>") @lnurlp_ext.route("/print/<link_id>")
def print_qr(link_id): async def print_qr(link_id):
link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.") link = get_pay_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Pay link does not exist.")
return await render_template("lnurlp/print_qr.html", link=link)
return render_template("lnurlp/print_qr.html", link=link)

14
lnbits/extensions/lnurlp/views_api.py

@ -1,5 +1,5 @@
import hashlib import hashlib
from flask import g, jsonify, request, url_for from quart import g, jsonify, request, url_for
from http import HTTPStatus from http import HTTPStatus
from lnurl import LnurlPayResponse, LnurlPayActionResponse from lnurl import LnurlPayResponse, LnurlPayActionResponse
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
@ -22,7 +22,7 @@ from .crud import (
@lnurlp_ext.route("/api/v1/links", methods=["GET"]) @lnurlp_ext.route("/api/v1/links", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_links(): async def api_links():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -42,7 +42,7 @@ def api_links():
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_link_retrieve(link_id): async def api_link_retrieve(link_id):
link = get_pay_link(link_id) link = get_pay_link(link_id)
if not link: if not link:
@ -63,7 +63,7 @@ def api_link_retrieve(link_id):
"amount": {"type": "integer", "min": 1, "required": True}, "amount": {"type": "integer", "min": 1, "required": True},
} }
) )
def api_link_create_or_update(link_id=None): async def api_link_create_or_update(link_id=None):
if link_id: if link_id:
link = get_pay_link(link_id) link = get_pay_link(link_id)
@ -82,7 +82,7 @@ def api_link_create_or_update(link_id=None):
@lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) @lnurlp_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_link_delete(link_id): async def api_link_delete(link_id):
link = get_pay_link(link_id) link = get_pay_link(link_id)
if not link: if not link:
@ -97,7 +97,7 @@ def api_link_delete(link_id):
@lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/lnurl/<link_id>", methods=["GET"])
def api_lnurl_response(link_id): async def api_lnurl_response(link_id):
link = increment_pay_link(link_id, served_meta=1) link = increment_pay_link(link_id, served_meta=1)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK
@ -116,7 +116,7 @@ def api_lnurl_response(link_id):
@lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"]) @lnurlp_ext.route("/api/v1/lnurl/cb/<link_id>", methods=["GET"])
def api_lnurl_callback(link_id): async def api_lnurl_callback(link_id):
link = increment_pay_link(link_id, served_pr=1) link = increment_pay_link(link_id, served_pr=1)
if not link: if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK

2
lnbits/extensions/paywall/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates") paywall_ext: Blueprint = Blueprint("paywall", __name__, static_folder="static", template_folder="templates")

1
lnbits/extensions/paywall/templates/paywall/display.html

@ -69,7 +69,6 @@
</div> </div>
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

11
lnbits/extensions/paywall/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -10,12 +10,11 @@ from .crud import get_paywall
@paywall_ext.route("/") @paywall_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("paywall/index.html", user=g.user) return await render_template("paywall/index.html", user=g.user)
@paywall_ext.route("/<paywall_id>") @paywall_ext.route("/<paywall_id>")
def display(paywall_id): async def display(paywall_id):
paywall = get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.") paywall = get_paywall(paywall_id) or abort(HTTPStatus.NOT_FOUND, "Paywall does not exist.")
return await render_template("paywall/display.html", paywall=paywall)
return render_template("paywall/display.html", paywall=paywall)

12
lnbits/extensions/paywall/views_api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
@ -11,7 +11,7 @@ from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
@paywall_ext.route("/api/v1/paywalls", methods=["GET"]) @paywall_ext.route("/api/v1/paywalls", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_paywalls(): async def api_paywalls():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -31,7 +31,7 @@ def api_paywalls():
"remembers": {"type": "boolean", "required": True}, "remembers": {"type": "boolean", "required": True},
} }
) )
def api_paywall_create(): async def api_paywall_create():
paywall = create_paywall(wallet_id=g.wallet.id, **g.data) paywall = create_paywall(wallet_id=g.wallet.id, **g.data)
return jsonify(paywall._asdict()), HTTPStatus.CREATED return jsonify(paywall._asdict()), HTTPStatus.CREATED
@ -39,7 +39,7 @@ def api_paywall_create():
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_paywall_delete(paywall_id): async def api_paywall_delete(paywall_id):
paywall = get_paywall(paywall_id) paywall = get_paywall(paywall_id)
if not paywall: if not paywall:
@ -55,7 +55,7 @@ def api_paywall_delete(paywall_id):
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) @api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
def api_paywall_create_invoice(paywall_id): async def api_paywall_create_invoice(paywall_id):
paywall = get_paywall(paywall_id) paywall = get_paywall(paywall_id)
if g.data["amount"] < paywall.amount: if g.data["amount"] < paywall.amount:
@ -74,7 +74,7 @@ def api_paywall_create_invoice(paywall_id):
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"]) @paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
@api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}}) @api_validate_post_request(schema={"payment_hash": {"type": "string", "empty": False, "required": True}})
def api_paywal_check_invoice(paywall_id): async def api_paywal_check_invoice(paywall_id):
paywall = get_paywall(paywall_id) paywall = get_paywall(paywall_id)
if not paywall: if not paywall:

2
lnbits/extensions/tpos/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
tpos_ext: Blueprint = Blueprint("tpos", __name__, static_folder="static", template_folder="templates") tpos_ext: Blueprint = Blueprint("tpos", __name__, static_folder="static", template_folder="templates")

1
lnbits/extensions/tpos/templates/tpos/tpos.html

@ -152,7 +152,6 @@
} }
</style> </style>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

10
lnbits/extensions/tpos/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -10,12 +10,12 @@ from .crud import get_tpos
@tpos_ext.route("/") @tpos_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("tpos/index.html", user=g.user) return await render_template("tpos/index.html", user=g.user)
@tpos_ext.route("/<tpos_id>") @tpos_ext.route("/<tpos_id>")
def tpos(tpos_id): async def tpos(tpos_id):
tpos = get_tpos(tpos_id) or abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.") tpos = get_tpos(tpos_id) or abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
return render_template("tpos/tpos.html", tpos=tpos) return await render_template("tpos/tpos.html", tpos=tpos)

12
lnbits/extensions/tpos/views_api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet from lnbits.core.crud import get_user, get_wallet
@ -11,7 +11,7 @@ from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
@tpos_ext.route("/api/v1/tposs", methods=["GET"]) @tpos_ext.route("/api/v1/tposs", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_tposs(): async def api_tposs():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -28,7 +28,7 @@ def api_tposs():
"currency": {"type": "string", "empty": False, "required": True}, "currency": {"type": "string", "empty": False, "required": True},
} }
) )
def api_tpos_create(): async def api_tpos_create():
tpos = create_tpos(wallet_id=g.wallet.id, **g.data) tpos = create_tpos(wallet_id=g.wallet.id, **g.data)
return jsonify(tpos._asdict()), HTTPStatus.CREATED return jsonify(tpos._asdict()), HTTPStatus.CREATED
@ -36,7 +36,7 @@ def api_tpos_create():
@tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>", methods=["DELETE"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
def api_tpos_delete(tpos_id): async def api_tpos_delete(tpos_id):
tpos = get_tpos(tpos_id) tpos = get_tpos(tpos_id)
if not tpos: if not tpos:
@ -52,7 +52,7 @@ def api_tpos_delete(tpos_id):
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/", methods=["POST"])
@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) @api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}})
def api_tpos_create_invoice(tpos_id): async def api_tpos_create_invoice(tpos_id):
tpos = get_tpos(tpos_id) tpos = get_tpos(tpos_id)
if not tpos: if not tpos:
@ -69,7 +69,7 @@ def api_tpos_create_invoice(tpos_id):
@tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<payment_hash>", methods=["GET"]) @tpos_ext.route("/api/v1/tposs/<tpos_id>/invoices/<payment_hash>", methods=["GET"])
def api_tpos_check_invoice(tpos_id, payment_hash): async def api_tpos_check_invoice(tpos_id, payment_hash):
tpos = get_tpos(tpos_id) tpos = get_tpos(tpos_id)
if not tpos: if not tpos:

2
lnbits/extensions/usermanager/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates") usermanager_ext: Blueprint = Blueprint("usermanager", __name__, static_folder="static", template_folder="templates")

9
lnbits/extensions/usermanager/views.py

@ -1,13 +1,10 @@
from flask import g, abort, render_template, jsonify from quart import g, render_template
import json
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.usermanager import usermanager_ext from lnbits.extensions.usermanager import usermanager_ext
from lnbits.db import open_ext_db
@usermanager_ext.route("/") @usermanager_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return await render_template("usermanager/index.html", user=g.user)
return render_template("usermanager/index.html", user=g.user)

27
lnbits/extensions/usermanager/views_api.py

@ -1,4 +1,4 @@
from flask import g, jsonify, request from quart import g, jsonify
from http import HTTPStatus from http import HTTPStatus
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
@ -17,12 +17,7 @@ from .crud import (
get_usermanager_wallets, get_usermanager_wallets,
delete_usermanager_wallet, delete_usermanager_wallet,
) )
from lnbits.core.services import create_invoice from lnbits.core import update_user_extension
from base64 import urlsafe_b64encode
from uuid import uuid4
from lnbits.db import open_ext_db
from ...core import update_user_extension
### Users ### Users
@ -30,7 +25,7 @@ from ...core import update_user_extension
@usermanager_ext.route("/api/v1/users", methods=["GET"]) @usermanager_ext.route("/api/v1/users", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_users(): async def api_usermanager_users():
user_id = g.wallet.user user_id = g.wallet.user
return jsonify([user._asdict() for user in get_usermanager_users(user_id)]), HTTPStatus.OK return jsonify([user._asdict() for user in get_usermanager_users(user_id)]), HTTPStatus.OK
@ -44,14 +39,14 @@ def api_usermanager_users():
"wallet_name": {"type": "string", "empty": False, "required": True}, "wallet_name": {"type": "string", "empty": False, "required": True},
} }
) )
def api_usermanager_users_create(): async def api_usermanager_users_create():
user = create_usermanager_user(g.data["user_name"], g.data["wallet_name"], g.data["admin_id"]) user = create_usermanager_user(g.data["user_name"], g.data["wallet_name"], g.data["admin_id"])
return jsonify(user._asdict()), HTTPStatus.CREATED return jsonify(user._asdict()), HTTPStatus.CREATED
@usermanager_ext.route("/api/v1/users/<user_id>", methods=["DELETE"]) @usermanager_ext.route("/api/v1/users/<user_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_users_delete(user_id): async def api_usermanager_users_delete(user_id):
user = get_usermanager_user(user_id) user = get_usermanager_user(user_id)
if not user: if not user:
return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND return jsonify({"message": "User does not exist."}), HTTPStatus.NOT_FOUND
@ -71,7 +66,7 @@ def api_usermanager_users_delete(user_id):
"active": {"type": "boolean", "required": True}, "active": {"type": "boolean", "required": True},
} }
) )
def api_usermanager_activate_extension(): async def api_usermanager_activate_extension():
user = get_user(g.data["userid"]) user = get_user(g.data["userid"])
if not user: if not user:
return jsonify({"error": "no such user"}), HTTPStatus.NO_CONTENT return jsonify({"error": "no such user"}), HTTPStatus.NO_CONTENT
@ -84,7 +79,7 @@ def api_usermanager_activate_extension():
@usermanager_ext.route("/api/v1/wallets", methods=["GET"]) @usermanager_ext.route("/api/v1/wallets", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_wallets(): async def api_usermanager_wallets():
user_id = g.wallet.user user_id = g.wallet.user
return jsonify([wallet._asdict() for wallet in get_usermanager_wallets(user_id)]), HTTPStatus.OK return jsonify([wallet._asdict() for wallet in get_usermanager_wallets(user_id)]), HTTPStatus.OK
@ -98,27 +93,27 @@ def api_usermanager_wallets():
"admin_id": {"type": "string", "empty": False, "required": True}, "admin_id": {"type": "string", "empty": False, "required": True},
} }
) )
def api_usermanager_wallets_create(): async def api_usermanager_wallets_create():
user = create_usermanager_wallet(g.data["user_id"], g.data["wallet_name"], g.data["admin_id"]) user = create_usermanager_wallet(g.data["user_id"], g.data["wallet_name"], g.data["admin_id"])
return jsonify(user._asdict()), HTTPStatus.CREATED return jsonify(user._asdict()), HTTPStatus.CREATED
@usermanager_ext.route("/api/v1/wallets<wallet_id>", methods=["GET"]) @usermanager_ext.route("/api/v1/wallets<wallet_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_wallet_transactions(wallet_id): async def api_usermanager_wallet_transactions(wallet_id):
return jsonify(get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK return jsonify(get_usermanager_wallet_transactions(wallet_id)), HTTPStatus.OK
@usermanager_ext.route("/api/v1/wallets/<user_id>", methods=["GET"]) @usermanager_ext.route("/api/v1/wallets/<user_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_wallet_balances(user_id): async def api_usermanager_wallet_balances(user_id):
return jsonify(get_usermanager_wallet_balances(user_id)), HTTPStatus.OK return jsonify(get_usermanager_wallet_balances(user_id)), HTTPStatus.OK
@usermanager_ext.route("/api/v1/wallets/<wallet_id>", methods=["DELETE"]) @usermanager_ext.route("/api/v1/wallets/<wallet_id>", methods=["DELETE"])
@api_check_wallet_key(key_type="invoice") @api_check_wallet_key(key_type="invoice")
def api_usermanager_wallets_delete(wallet_id): async def api_usermanager_wallets_delete(wallet_id):
wallet = get_usermanager_wallet(wallet_id) wallet = get_usermanager_wallet(wallet_id)
print(wallet.id) print(wallet.id)
if not wallet: if not wallet:

2
lnbits/extensions/withdraw/__init__.py

@ -1,4 +1,4 @@
from flask import Blueprint from quart import Blueprint
withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates") withdraw_ext: Blueprint = Blueprint("withdraw", __name__, static_folder="static", template_folder="templates")

2
lnbits/extensions/withdraw/models.py

@ -1,4 +1,4 @@
from flask import url_for from quart import url_for
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple from typing import NamedTuple

2
lnbits/extensions/withdraw/templates/withdraw/display.html

@ -43,8 +43,6 @@
</div> </div>
</div> </div>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

7
lnbits/extensions/withdraw/templates/withdraw/index.html

@ -1,10 +1,7 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }} %} {% block scripts %} {{ window_vars(user) }}
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script> <script type="text/javascript" src="/withdraw/static/js/index.js"></script>
{% assets filters='rjsmin', output='__bundle__/withdraw/index.js', {% endblock %} {% block page %}
'withdraw/js/index.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endblock %} {% block page %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md"> <div class="col-12 col-md-7 q-gutter-y-md">
<q-card> <q-card>

12
lnbits/extensions/withdraw/templates/withdraw/print_qr.html

@ -1,5 +1,4 @@
<!DOCTYPE html> {% extends "print.html" %} {% block page %}
{% block page %}
<div class="row justify-center"> <div class="row justify-center">
<div class="col-12 col-sm-8 col-lg-6 text-center" id="vue"> <div class="col-12 col-sm-8 col-lg-6 text-center" id="vue">
@ -50,15 +49,6 @@
} }
</style> </style>
{% endblock %} {% block scripts %} {% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='vendor/vue@2.6.12/vue.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/vuex@3.5.1/vuex.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/vue-router@3.4.3/vue-router.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/quasar@1.13.2/quasar.umd.js') }}"></script>
<script
type="text/javascript"
src="/static/__bundle__/base.js?a52a989e"
></script>
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<script> <script>
Vue.component(VueQrcode.name, VueQrcode) Vue.component(VueQrcode.name, VueQrcode)

16
lnbits/extensions/withdraw/views.py

@ -1,4 +1,4 @@
from flask import g, abort, render_template from quart import g, abort, render_template
from http import HTTPStatus from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids from lnbits.decorators import check_user_exists, validate_uuids
@ -10,21 +10,21 @@ from .crud import get_withdraw_link, chunks
@withdraw_ext.route("/") @withdraw_ext.route("/")
@validate_uuids(["usr"], required=True) @validate_uuids(["usr"], required=True)
@check_user_exists() @check_user_exists()
def index(): async def index():
return render_template("withdraw/index.html", user=g.user) return await render_template("withdraw/index.html", user=g.user)
@withdraw_ext.route("/<link_id>") @withdraw_ext.route("/<link_id>")
def display(link_id): async def display(link_id):
link = get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") link = get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.")
return render_template("withdraw/display.html", link=link, unique=True) return await render_template("withdraw/display.html", link=link, unique=True)
@withdraw_ext.route("/print/<link_id>") @withdraw_ext.route("/print/<link_id>")
def print_qr(link_id): async def print_qr(link_id):
link = get_withdraw_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") link = get_withdraw_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.")
if link.uses == 0: if link.uses == 0:
return render_template("withdraw/print_qr.html", link=link, unique=False) return await render_template("withdraw/print_qr.html", link=link, unique=False)
links = [] links = []
count = 0 count = 0
for x in link.usescsv.split(","): for x in link.usescsv.split(","):
@ -33,4 +33,4 @@ def print_qr(link_id):
count = count + 1 count = count + 1
page_link = list(chunks(links, 2)) page_link = list(chunks(links, 2))
linked = list(chunks(page_link, 5)) linked = list(chunks(page_link, 5))
return render_template("withdraw/print_qr.html", link=linked, unique=True) return await render_template("withdraw/print_qr.html", link=linked, unique=True)

16
lnbits/extensions/withdraw/views_api.py

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from flask import g, jsonify, request from quart import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
import shortuuid # type: ignore import shortuuid # type: ignore
@ -21,7 +21,7 @@ from .crud import (
@withdraw_ext.route("/api/v1/links", methods=["GET"]) @withdraw_ext.route("/api/v1/links", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_links(): async def api_links():
wallet_ids = [g.wallet.id] wallet_ids = [g.wallet.id]
if "all_wallets" in request.args: if "all_wallets" in request.args:
@ -40,7 +40,7 @@ def api_links():
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"]) @withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice") @api_check_wallet_key("invoice")
def api_link_retrieve(link_id): async def api_link_retrieve(link_id):
link = get_withdraw_link(link_id, 0) link = get_withdraw_link(link_id, 0)
if not link: if not link:
@ -65,7 +65,7 @@ def api_link_retrieve(link_id):
"is_unique": {"type": "boolean", "required": True}, "is_unique": {"type": "boolean", "required": True},
} }
) )
def api_link_create_or_update(link_id=None): async def api_link_create_or_update(link_id=None):
if g.data["max_withdrawable"] < g.data["min_withdrawable"]: if g.data["max_withdrawable"] < g.data["min_withdrawable"]:
return ( return (
jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}), jsonify({"message": "`max_withdrawable` needs to be at least `min_withdrawable`."}),
@ -95,7 +95,7 @@ def api_link_create_or_update(link_id=None):
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"]) @withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
def api_link_delete(link_id): async def api_link_delete(link_id):
link = get_withdraw_link(link_id) link = get_withdraw_link(link_id)
if not link: if not link:
@ -113,7 +113,7 @@ def api_link_delete(link_id):
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"])
def api_lnurl_response(unique_hash): async def api_lnurl_response(unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = get_withdraw_link_by_hash(unique_hash)
if not link: if not link:
@ -134,7 +134,7 @@ def api_lnurl_response(unique_hash):
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"])
def api_lnurl_multi_response(unique_hash, id_unique_hash): async def api_lnurl_multi_response(unique_hash, id_unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = get_withdraw_link_by_hash(unique_hash)
if not link: if not link:
@ -163,7 +163,7 @@ def api_lnurl_multi_response(unique_hash, id_unique_hash):
@withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"])
def api_lnurl_callback(unique_hash): async def api_lnurl_callback(unique_hash):
link = get_withdraw_link_by_hash(unique_hash) link = get_withdraw_link_by_hash(unique_hash)
k1 = request.args.get("k1", type=str) k1 = request.args.get("k1", type=str)
payment_request = request.args.get("pr", type=str) payment_request = request.args.get("pr", type=str)

52
lnbits/helpers.py

@ -1,5 +1,6 @@
import json import json
import os import os
import glob
import shortuuid # type: ignore import shortuuid # type: ignore
from typing import List, NamedTuple, Optional from typing import List, NamedTuple, Optional
@ -54,3 +55,54 @@ def get_valid_extensions() -> List[Extension]:
def urlsafe_short_hash() -> str: def urlsafe_short_hash() -> str:
return shortuuid.uuid() return shortuuid.uuid()
def get_js_vendored(prefer_minified: bool = False) -> List[str]:
paths = get_vendored(".js", prefer_minified)
def sorter(key: str):
if "moment@" in key:
return 1
if "vue@" in key:
return 2
if "vue-router@" in key:
return 3
if "polyfills" in key:
return 4
return 9
return sorted(paths, key=sorter)
def get_css_vendored(prefer_minified: bool = False) -> List[str]:
return get_vendored(".css", prefer_minified)
def get_vendored(ext: str, prefer_minified: bool = False) -> List[str]:
paths: List[str] = []
for path in glob.glob(os.path.join(LNBITS_PATH, "static/vendor/**"), recursive=True):
if path.endswith(".min" + ext):
# path is minified
unminified = path.replace(".min" + ext, ext)
if prefer_minified:
paths.append(path)
if unminified in paths:
paths.remove(unminified)
elif unminified not in paths:
paths.append(path)
elif path.endswith(ext):
# path is not minified
minified = path.replace(ext, ".min" + ext)
if not prefer_minified:
paths.append(path)
if minified in paths:
paths.remove(minified)
elif minified not in paths:
paths.append(path)
return paths
def url_for_vendored(abspath: str) -> str:
return "/" + os.path.relpath(abspath, LNBITS_PATH)

100
lnbits/proxy_fix.py

@ -0,0 +1,100 @@
from typing import Optional, List
from urllib.request import parse_http_list as _parse_list_header
from quart import request
class ProxyFix:
def __init__(self, app=None, x_for: int = 1, x_proto: int = 1, x_host: int = 0, x_port: int = 0, x_prefix: int = 0):
self.app = app
self.x_for = x_for
self.x_proto = x_proto
self.x_host = x_host
self.x_port = x_port
self.x_prefix = x_prefix
if app:
self.init_app(app)
def init_app(self, app):
@app.before_request
async def before_request():
x_for = self._get_real_value(self.x_for, request.headers.get("X-Forwarded-For"))
if x_for:
request.headers["Remote-Addr"] = x_for
x_proto = self._get_real_value(self.x_proto, request.headers.get("X-Forwarded-Proto"))
if x_proto:
request.scheme = x_proto
x_host = self._get_real_value(self.x_host, request.headers.get("X-Forwarded-Host"))
if x_host:
request.headers["host"] = x_host.lower()
parts = x_host.split(":", 1)
# environ["SERVER_NAME"] = parts[0]
# if len(parts) == 2:
# environ["SERVER_PORT"] = parts[1]
x_port = self._get_real_value(self.x_port, request.headers.get("X-Forwarded-Port"))
if x_port:
host = request.host
if host:
parts = host.split(":", 1)
host = parts[0] if len(parts) == 2 else host
request.headers["host"] = f"{host}:{x_port}"
# environ["SERVER_PORT"] = x_port
def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
"""Get the real value from a list header based on the configured
number of trusted proxies.
:param trusted: Number of values to trust in the header.
:param value: Comma separated list header value to parse.
:return: The real value, or ``None`` if there are fewer values
than the number of trusted proxies.
.. versionchanged:: 1.0
Renamed from ``_get_trusted_comma``.
.. versionadded:: 0.15
"""
if not (trusted and value):
return None
values = self.parse_list_header(value)
if len(values) >= trusted:
return values[-trusted]
return None
def parse_list_header(self, value: str) -> List[str]:
result = []
for item in _parse_list_header(value):
if item[:1] == item[-1:] == '"':
item = self.unquote_header_value(item[1:-1])
result.append(item)
return result
def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
This does not use the real unquoting but what browsers are actually
using for quoting.
.. versionadded:: 0.5
:param value: the header value to unquote.
:param is_filename: The value represents a filename or path.
"""
if value and value[0] == value[-1] == '"':
# this is not the real unquoting, but fixing this so that the
# RFC is met will result in bugs with internet explorer and
# probably some other browsers as well. IE for example is
# uploading files with "C:\foo\bar.txt" as filename
value = value[1:-1]
# if this is a filename and the starting characters look like
# a UNC path, then just return the value without quotes. Using the
# replace sequence below on a UNC path has the effect of turning
# the leading double slash into a single slash and then
# _fix_ie_filename() doesn't work correctly. See #458.
if not is_filename or value[:2] != "\\\\":
return value.replace("\\\\", "\\").replace('\\"', '"')
return value
# host, request.root_path, subdomain, request.scheme, request.method, request.path, request.query_string.decode(),

6
lnbits/settings.py

@ -11,8 +11,10 @@ env.read_env()
wallets_module = importlib.import_module("lnbits.wallets") wallets_module = importlib.import_module("lnbits.wallets")
wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet")) wallet_class = getattr(wallets_module, env.str("LNBITS_BACKEND_WALLET_CLASS", default="VoidWallet"))
ENV = env.str("FLASK_ENV", default="production") ENV = env.str("QUART_ENV", default="production")
DEBUG = ENV == "development" DEBUG = env.bool("QUART_DEBUG", default=False) or ENV == "development"
HOST = env.str("HOST", default="127.0.0.1")
PORT = env.int("PORT", default=5000)
LNBITS_PATH = path.dirname(path.realpath(__file__)) LNBITS_PATH = path.dirname(path.realpath(__file__))
LNBITS_DATA_FOLDER = env.str("LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data")) LNBITS_DATA_FOLDER = env.str("LNBITS_DATA_FOLDER", default=path.join(LNBITS_PATH, "data"))

1
lnbits/static/css/.gitignore

@ -0,0 +1 @@
base.css

77
lnbits/static/css/base.css

@ -1,77 +0,0 @@
[v-cloak] {
display: none; }
.bg-lnbits-dark {
background-color: #1f2234; }
body.body--dark, body.body--dark .q-drawer--dark, body.body--dark .q-menu--dark {
background: #1f2234; }
body.body--dark .q-card--dark {
background: #333646; }
body.body--dark .q-table--dark {
background: transparent; }
body.body--light, body.body--light .q-drawer {
background: whitesmoke; }
body.body--dark .q-field--error .text-negative,
body.body--dark .q-field--error .q-field__messages {
color: yellow !important; }
.lnbits-drawer__q-list .q-item {
padding-top: 5px !important;
padding-bottom: 5px !important;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px; }
.lnbits-drawer__q-list .q-item.q-item--active {
color: inherit;
font-weight: bold; }
.lnbits__dialog-card {
width: 500px; }
.q-table--dense th:first-child, .q-table--dense td:first-child,
.q-table--dense .q-table__bottom {
padding-left: 6px !important; }
.q-table--dense th:last-child, .q-table--dense td:last-child,
.q-table--dense .q-table__bottom {
padding-right: 6px !important; }
a.inherit {
color: inherit;
text-decoration: none; }
video {
border-radius: 3px; }
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(../fonts/material-icons-v50.woff2) format('woff2'); }
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale; }
.text-wrap {
word-wrap: break-word;
word-break: break-all;
}
.mono {
font-family: monospace;
}

393
lnbits/static/vendor/bolt11/decoder.js

@ -4,233 +4,344 @@
//TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided. //TODO - A reader MUST use the n field to validate the signature instead of performing signature recovery if a valid n field is provided.
function decode(paymentRequest) { function decode(paymentRequest) {
let input = paymentRequest.toLowerCase(); let input = paymentRequest.toLowerCase()
let splitPosition = input.lastIndexOf('1'); let splitPosition = input.lastIndexOf('1')
let humanReadablePart = input.substring(0, splitPosition); let humanReadablePart = input.substring(0, splitPosition)
let data = input.substring(splitPosition + 1, input.length - 6); let data = input.substring(splitPosition + 1, input.length - 6)
let checksum = input.substring(input.length - 6, input.length); let checksum = input.substring(input.length - 6, input.length)
if (!verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))) { if (
throw 'Malformed request: checksum is incorrect'; // A reader MUST fail if the checksum is incorrect. !verify_checksum(humanReadablePart, bech32ToFiveBitArray(data + checksum))
) {
throw 'Malformed request: checksum is incorrect' // A reader MUST fail if the checksum is incorrect.
} }
return { return {
'human_readable_part': decodeHumanReadablePart(humanReadablePart), human_readable_part: decodeHumanReadablePart(humanReadablePart),
'data': decodeData(data, humanReadablePart), data: decodeData(data, humanReadablePart),
'checksum': checksum checksum: checksum
} }
} }
function decodeHumanReadablePart(humanReadablePart) { function decodeHumanReadablePart(humanReadablePart) {
let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb']; let prefixes = ['lnbc', 'lntb', 'lnbcrt', 'lnsb']
let prefix; let prefix
prefixes.forEach(value => { prefixes.forEach(value => {
if (humanReadablePart.substring(0, value.length) === value) { if (humanReadablePart.substring(0, value.length) === value) {
prefix = value; prefix = value
} }
}); })
if (prefix == null) throw 'Malformed request: unknown prefix'; // A reader MUST fail if it does not understand the prefix. if (prefix == null) throw 'Malformed request: unknown prefix' // A reader MUST fail if it does not understand the prefix.
let amount = decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length)); let amount = decodeAmount(
humanReadablePart.substring(prefix.length, humanReadablePart.length)
)
return { return {
'prefix': prefix, prefix: prefix,
'amount': amount amount: amount
} }
} }
function decodeData(data, humanReadablePart) { function decodeData(data, humanReadablePart) {
let date32 = data.substring(0, 7); let date32 = data.substring(0, 7)
let dateEpoch = bech32ToInt(date32); let dateEpoch = bech32ToInt(date32)
let signature = data.substring(data.length - 104, data.length); let signature = data.substring(data.length - 104, data.length)
let tagData = data.substring(7, data.length - 104); let tagData = data.substring(7, data.length - 104)
let decodedTags = decodeTags(tagData); let decodedTags = decodeTags(tagData)
let value = bech32ToFiveBitArray(date32 + tagData); let value = bech32ToFiveBitArray(date32 + tagData)
value = fiveBitArrayTo8BitArray(value, true); value = fiveBitArrayTo8BitArray(value, true)
value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value)); value = textToHexString(humanReadablePart).concat(byteArrayToHexString(value))
return { return {
'time_stamp': dateEpoch, time_stamp: dateEpoch,
'tags': decodedTags, tags: decodedTags,
'signature': decodeSignature(signature), signature: decodeSignature(signature),
'signing_data': value signing_data: value
} }
} }
function decodeSignature(signature) { function decodeSignature(signature) {
let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature)); let data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(signature))
let recoveryFlag = data[data.length - 1]; let recoveryFlag = data[data.length - 1]
let r = byteArrayToHexString(data.slice(0, 32)); let r = byteArrayToHexString(data.slice(0, 32))
let s = byteArrayToHexString(data.slice(32, data.length - 1)); let s = byteArrayToHexString(data.slice(32, data.length - 1))
return { return {
'r': r, r: r,
's': s, s: s,
'recovery_flag': recoveryFlag recovery_flag: recoveryFlag
} }
} }
function decodeAmount(str) { function decodeAmount(str) {
let multiplier = str.charAt(str.length - 1); let multiplier = str.charAt(str.length - 1)
let amount = str.substring(0, str.length - 1); let amount = str.substring(0, str.length - 1)
if (amount.substring(0, 1) === '0') { if (amount.substring(0, 1) === '0') {
throw 'Malformed request: amount cannot contain leading zeros'; throw 'Malformed request: amount cannot contain leading zeros'
} }
amount = Number(amount); amount = Number(amount)
if (amount < 0 || !Number.isInteger(amount)) { if (amount < 0 || !Number.isInteger(amount)) {
throw 'Malformed request: amount must be a positive decimal integer'; // A reader SHOULD fail if amount contains a non-digit throw 'Malformed request: amount must be a positive decimal integer' // A reader SHOULD fail if amount contains a non-digit
} }
switch (multiplier) { switch (multiplier) {
case '': case '':
return 'Any amount'; // A reader SHOULD indicate if amount is unspecified return 'Any amount' // A reader SHOULD indicate if amount is unspecified
case 'p': case 'p':
return amount / 10; return amount / 10
case 'n': case 'n':
return amount * 100; return amount * 100
case 'u': case 'u':
return amount * 100000; return amount * 100000
case 'm': case 'm':
return amount * 100000000; return amount * 100000000
default: default:
// A reader SHOULD fail if amount is followed by anything except a defined multiplier. // A reader SHOULD fail if amount is followed by anything except a defined multiplier.
throw 'Malformed request: undefined amount multiplier'; throw 'Malformed request: undefined amount multiplier'
} }
} }
function decodeTags(tagData) { function decodeTags(tagData) {
let tags = extractTags(tagData); let tags = extractTags(tagData)
let decodedTags = []; let decodedTags = []
tags.forEach(value => decodedTags.push(decodeTag(value.type, value.length, value.data))); tags.forEach(value =>
return decodedTags; decodedTags.push(decodeTag(value.type, value.length, value.data))
)
return decodedTags
} }
function extractTags(str) { function extractTags(str) {
let tags = []; let tags = []
while (str.length > 0) { while (str.length > 0) {
let type = str.charAt(0); let type = str.charAt(0)
let dataLength = bech32ToInt(str.substring(1, 3)); let dataLength = bech32ToInt(str.substring(1, 3))
let data = str.substring(3, dataLength + 3); let data = str.substring(3, dataLength + 3)
tags.push({ tags.push({
'type': type, type: type,
'length': dataLength, length: dataLength,
'data': data data: data
}); })
str = str.substring(3 + dataLength, str.length); str = str.substring(3 + dataLength, str.length)
} }
return tags; return tags
} }
function decodeTag(type, length, data) { function decodeTag(type, length, data) {
switch (type) { switch (type) {
case 'p': case 'p':
if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52 if (length !== 52) break // A reader MUST skip over a 'p' field that does not have data_length 52
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'payment_hash', description: 'payment_hash',
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) value: byteArrayToHexString(
}; fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
)
}
case 'd': case 'd':
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'description', description: 'description',
'value': bech32ToUTF8String(data) value: bech32ToUTF8String(data)
}; }
case 'n': case 'n':
if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53 if (length !== 53) break // A reader MUST skip over a 'n' field that does not have data_length 53
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'payee_public_key', description: 'payee_public_key',
'value': byteArrayToHexString(fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))) value: byteArrayToHexString(
}; fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
)
}
case 'h': case 'h':
if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52 if (length !== 52) break // A reader MUST skip over a 'h' field that does not have data_length 52
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'description_hash', description: 'description_hash',
'value': data value: data
}; }
case 'x': case 'x':
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'expiry', description: 'expiry',
'value': bech32ToInt(data) value: bech32ToInt(data)
}; }
case 'c': case 'c':
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'min_final_cltv_expiry', description: 'min_final_cltv_expiry',
'value': bech32ToInt(data) value: bech32ToInt(data)
}; }
case 'f': case 'f':
let version = bech32ToFiveBitArray(data.charAt(0))[0]; let version = bech32ToFiveBitArray(data.charAt(0))[0]
if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version. if (version < 0 || version > 18) break // a reader MUST skip over an f field with unknown version.
data = data.substring(1, data.length); data = data.substring(1, data.length)
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'fallback_address', description: 'fallback_address',
'value': { value: {
'version': version, version: version,
'fallback_address': data fallback_address: data
} }
}; }
case 'r': case 'r':
data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data)); data = fiveBitArrayTo8BitArray(bech32ToFiveBitArray(data))
let pubkey = data.slice(0, 33); let pubkey = data.slice(0, 33)
let shortChannelId = data.slice(33, 41); let shortChannelId = data.slice(33, 41)
let feeBaseMsat = data.slice(41, 45); let feeBaseMsat = data.slice(41, 45)
let feeProportionalMillionths = data.slice(45, 49); let feeProportionalMillionths = data.slice(45, 49)
let cltvExpiryDelta = data.slice(49, 51); let cltvExpiryDelta = data.slice(49, 51)
return { return {
'type': type, type: type,
'length': length, length: length,
'description': 'routing_information', description: 'routing_information',
'value': { value: {
'public_key': byteArrayToHexString(pubkey), public_key: byteArrayToHexString(pubkey),
'short_channel_id': byteArrayToHexString(shortChannelId), short_channel_id: byteArrayToHexString(shortChannelId),
'fee_base_msat': byteArrayToInt(feeBaseMsat), fee_base_msat: byteArrayToInt(feeBaseMsat),
'fee_proportional_millionths': byteArrayToInt(feeProportionalMillionths), fee_proportional_millionths: byteArrayToInt(
'cltv_expiry_delta': byteArrayToInt(cltvExpiryDelta) feeProportionalMillionths
} ),
}; cltv_expiry_delta: byteArrayToInt(cltvExpiryDelta)
}
}
default: default:
// reader MUST skip over unknown fields // reader MUST skip over unknown fields
} }
} }
function polymod(values) { function polymod(values) {
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
let chk = 1; let chk = 1
values.forEach((value) => { values.forEach(value => {
let b = (chk >> 25); let b = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value; chk = ((chk & 0x1ffffff) << 5) ^ value
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (((b >> i) & 1) === 1) { if (((b >> i) & 1) === 1) {
chk ^= GEN[i]; chk ^= GEN[i]
} else { } else {
chk ^= 0; chk ^= 0
} }
} }
}); })
return chk; return chk
} }
function expand(str) { function expand(str) {
let array = []; let array = []
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) >> 5); array.push(str.charCodeAt(i) >> 5)
} }
array.push(0); array.push(0)
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) & 31); array.push(str.charCodeAt(i) & 31)
} }
return array; return array
} }
function verify_checksum(hrp, data) { function verify_checksum(hrp, data) {
hrp = expand(hrp); hrp = expand(hrp)
let all = hrp.concat(data); let all = hrp.concat(data)
let bool = polymod(all); let bool = polymod(all)
return bool === 1; return bool === 1
}
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
function byteArrayToInt(byteArray) {
let value = 0
for (let i = 0; i < byteArray.length; ++i) {
value = (value << 8) + byteArray[i]
}
return value
}
function bech32ToInt(str) {
let sum = 0
for (let i = 0; i < str.length; i++) {
sum = sum * 32
sum = sum + bech32CharValues.indexOf(str.charAt(i))
}
return sum
}
function bech32ToFiveBitArray(str) {
let array = []
for (let i = 0; i < str.length; i++) {
array.push(bech32CharValues.indexOf(str.charAt(i)))
}
return array
}
function fiveBitArrayTo8BitArray(int5Array, includeOverflow) {
let count = 0
let buffer = 0
let byteArray = []
int5Array.forEach(value => {
buffer = (buffer << 5) + value
count += 5
if (count >= 8) {
byteArray.push((buffer >> (count - 8)) & 255)
count -= 8
}
})
if (includeOverflow && count > 0) {
byteArray.push((buffer << (8 - count)) & 255)
}
return byteArray
}
function bech32ToUTF8String(str) {
let int5Array = bech32ToFiveBitArray(str)
let byteArray = fiveBitArrayTo8BitArray(int5Array)
let utf8String = ''
for (let i = 0; i < byteArray.length; i++) {
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2)
}
return decodeURIComponent(utf8String)
}
function byteArrayToHexString(byteArray) {
return Array.prototype.map
.call(byteArray, function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2)
})
.join('')
}
function textToHexString(text) {
let hexString = ''
for (let i = 0; i < text.length; i++) {
hexString += text.charCodeAt(i).toString(16)
}
return hexString
}
function epochToDate(int) {
let date = new Date(int * 1000)
return date.toUTCString()
}
function isEmptyOrSpaces(str) {
return str === null || str.match(/^ *$/) !== null
}
function toFixed(x) {
if (Math.abs(x) < 1.0) {
var e = parseInt(x.toString().split('e-')[1])
if (e) {
x *= Math.pow(10, e - 1)
x = '0.' + new Array(e).join('0') + x.toString().substring(2)
}
} else {
var e = parseInt(x.toString().split('+')[1])
if (e > 20) {
e -= 20
x /= Math.pow(10, e)
x += new Array(e + 1).join('0')
}
}
return x
} }

96
lnbits/static/vendor/bolt11/utils.js

@ -1,96 +0,0 @@
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
function byteArrayToInt(byteArray) {
let value = 0;
for (let i = 0; i < byteArray.length; ++i) {
value = (value << 8) + byteArray[i];
}
return value;
}
function bech32ToInt(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum = sum * 32;
sum = sum + bech32CharValues.indexOf(str.charAt(i));
}
return sum;
}
function bech32ToFiveBitArray(str) {
let array = [];
for (let i = 0; i < str.length; i++) {
array.push(bech32CharValues.indexOf(str.charAt(i)));
}
return array;
}
function fiveBitArrayTo8BitArray(int5Array, includeOverflow) {
let count = 0;
let buffer = 0;
let byteArray = [];
int5Array.forEach((value) => {
buffer = (buffer << 5) + value;
count += 5;
if (count >= 8) {
byteArray.push(buffer >> (count - 8) & 255);
count -= 8;
}
});
if (includeOverflow && count > 0) {
byteArray.push(buffer << (8 - count) & 255);
}
return byteArray;
}
function bech32ToUTF8String(str) {
let int5Array = bech32ToFiveBitArray(str);
let byteArray = fiveBitArrayTo8BitArray(int5Array);
let utf8String = '';
for (let i = 0; i < byteArray.length; i++) {
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2);
}
return decodeURIComponent(utf8String);
}
function byteArrayToHexString(byteArray) {
return Array.prototype.map.call(byteArray, function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
function textToHexString(text) {
let hexString = '';
for (let i = 0; i < text.length; i++) {
hexString += text.charCodeAt(i).toString(16);
}
return hexString;
}
function epochToDate(int) {
let date = new Date(int * 1000);
return date.toUTCString();
}
function isEmptyOrSpaces(str){
return str === null || str.match(/^ *$/) !== null;
}
function toFixed(x) {
if (Math.abs(x) < 1.0) {
var e = parseInt(x.toString().split('e-')[1]);
if (e) {
x *= Math.pow(10,e-1);
x = '0.' + (new Array(e)).join('0') + x.toString().substring(2);
}
} else {
var e = parseInt(x.toString().split('+')[1]);
if (e > 20) {
e -= 20;
x /= Math.pow(10,e);
x += (new Array(e+1)).join('0');
}
}
return x;
}

39
lnbits/templates/base.html

@ -2,14 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<link {% for url in g.VENDORED_CSS %}
rel="stylesheet" <link rel="stylesheet" type="text/css" href="{{ url }}" />
type="text/css" {% endfor %}
href="{{ url_for('static', filename='vendor/quasar@1.13.2/quasar.min.css') }}" <!---->
/> <link rel="stylesheet" type="text/css" href="/static/css/base.css" />
{% assets 'base_css' %} {% block styles %}{% endblock %}
<link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}" />
{% endassets %} {% block styles %}{% endblock %}
<title> <title>
{% block title %} {% if SITE_TITLE != 'LNbits' %}{{ SITE_TITLE }}{% else {% block title %} {% if SITE_TITLE != 'LNbits' %}{{ SITE_TITLE }}{% else
%}LNbits{% endif %} {% endblock %} %}LNbits{% endif %} {% endblock %}
@ -108,21 +106,14 @@
{% endblock %} {% endblock %}
</q-layout> </q-layout>
{% block vue_templates %}{% endblock %} {% if DEBUG %} {% block vue_templates %}{% endblock %}
<script src="{{ url_for('static', filename='vendor/vue@2.6.12/vue.js') }}"></script> <!---->
<script src="{{ url_for('static', filename='vendor/vuex@3.5.1/vuex.js') }}"></script> {% for url in g.VENDORED_JS %}
<script src="{{ url_for('static', filename='vendor/vue-router@3.4.3/vue-router.js') }}"></script> <script src="{{ url }}"></script>
<script src="{{ url_for('static', filename='vendor/quasar@1.13.2/quasar.umd.js') }}"></script> {% endfor %}
{% else %} {% assets output='__bundle__/vue.js', <!---->
'vendor/quasar@1.13.2/quasar.ie.polyfills.umd.min.js', <script src="/static/js/base.js"></script>
'vendor/vue@2.6.12/vue.min.js', 'vendor/vue-router@3.4.3/vue-router.min.js', <script src="/static/js/components.js"></script>
'vendor/vuex@3.5.1/vuex.min.js', 'vendor/quasar@1.13.2/quasar.umd.min.js' %} {% block scripts %}{% endblock %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endif %} {% assets filters='rjsmin',
output='__bundle__/base.js', 'vendor/axios@0.20.0/axios.min.js',
'vendor/underscore@1.10.2/underscore.min.js', 'js/base.js',
'js/components.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

21
lnbits/templates/print.html

@ -2,11 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<link {% for url in g.VENDORED_CSS %}
rel="stylesheet" <link rel="stylesheet" type="text/css" href="{{ url }}" />
href="//fonts.googleapis.com/css?family=Material+Icons" {% endfor %}
type="text/css"
/>
<style> <style>
@page { @page {
size: A4 portrait; size: A4 portrait;
@ -38,13 +36,10 @@
</q-page-container> </q-page-container>
</q-layout> </q-layout>
{% if DEBUG %} {% for url in g.VENDORED_JS %}
<script src="{{ url_for('static', filename='vendor/vue@2.6.12/vue.js') }}"></script> <script src="{{ url }}"></script>
<script src="{{ url_for('static', filename='vendor/quasar@1.13.2/quasar.umd.js') }}"></script> {% endfor %}
{% else %} {% assets output='__bundle__/vue-print.js', <!---->
'vendor/quasar@1.13.2/quasar.ie.polyfills.umd.min.js', {% block scripts %}{% endblock %}
'vendor/vue@2.6.12/vue.min.js', 'vendor/quasar@1.13.2/quasar.umd.min.js' %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %} {% endif %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

44
requirements.txt

@ -1,30 +1,38 @@
bech32==1.2.0; python_version >= '3.5' aiofiles==0.5.0
bech32==1.2.0
bitstring==3.1.7 bitstring==3.1.7
blinker==1.4
brotli==1.0.9 brotli==1.0.9
cerberus==1.3.2 cerberus==1.3.2
certifi==2020.6.20 certifi==2020.6.20
chardet==3.0.4 chardet==3.0.4
click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' click==7.1.2
ecdsa==0.16.0 ecdsa==0.16.0
environs==8.0.0 environs==8.0.0
flask-assets==2.0 h11==0.10.0
flask-compress==1.5.0 h2==3.2.0
flask-cors==3.0.9 hpack==3.0.0
flask-talisman==0.7.0 hypercorn==0.10.2
flask==1.1.2 hyperframe==5.2.0
idna==2.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' idna==2.10
itsdangerous==1.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' itsdangerous==1.1.0
jinja2==2.11.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' jinja2==2.11.2
lnurl==0.3.5 lnurl==0.3.5
markupsafe==1.1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' markupsafe==1.1.1
marshmallow==3.7.1; python_version >= '3.5' marshmallow==3.8.0
pydantic==1.6.1; python_version >= '3.6' priority==1.3.0
pydantic==1.6.1
pyscss==1.3.7 pyscss==1.3.7
python-dotenv==0.14.0 python-dotenv==0.14.0
quart==0.13.1
quart-compress==0.2.1
quart-cors==0.3.0
requests==2.24.0 requests==2.24.0
secure==0.2.1
shortuuid==1.0.1 shortuuid==1.0.1
six==1.15.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' six==1.15.0
typing-extensions==3.7.4.3; python_version < '3.8' toml==0.10.1
urllib3==1.25.10; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' typing-extensions==3.7.4.3
webassets==2.0 urllib3==1.25.10
werkzeug==1.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' werkzeug==1.0.1
wsproto==0.15.0

5
tests/conftest.py

@ -4,9 +4,10 @@ from lnbits.app import create_app
@pytest.fixture @pytest.fixture
def client(): @pytest.mark.asyncio
async def client():
app = create_app() app = create_app()
app.config["TESTING"] = True app.config["TESTING"] = True
with app.test_client() as client: async with app.test_client() as client:
yield client yield client

10
tests/core/test_views.py

@ -1,3 +1,7 @@
def test_homepage(client): import pytest
r = client.get("/")
assert b"Add a new wallet" in r.data
@pytest.mark.asyncio
async def test_homepage(client):
r = await client.get("/")
assert b"Add a new wallet" in await r.get_data()

Loading…
Cancel
Save