Compare commits

...
54 Commits
Author SHA1 Message Date
ArcandGitHub 41a9689014 Update installation.md 2022-02-18 17:17:18 +00:00
ArcandGitHub 76edfa2134 Update installation.md 2022-02-18 17:16:49 +00:00
ArcandGitHub d7b3a28950 Update installation.md 2022-02-18 17:16:18 +00:00
ArcandGitHub 2c161a079d Update installation.md 2022-02-18 17:16:03 +00:00
ArcandGitHub 88df4266b7 Merge pull request #524 from lnbits/docs_api_add_unit_to_invoice
Fix fiat amount entry in invoice creation
2022-02-18 11:44:05 +00:00
callebtc 6496fac35f fill mask for fiat 2022-02-18 12:35:25 +01:00
Ben Arc 10492ec3d0 Changed bitcoin theme back 2022-02-17 23:40:29 +00:00
Ben Arc 302d79a30f Improved bitcoin theme 2022-02-17 23:33:55 +00:00
ArcandGitHub b88abd1a46 Merge pull request #527 from talvasconcelos/fix/instantWallet
Fix instant wallet form LNURLw
2022-02-17 23:21:58 +00:00
Ben Arc 573c4cf5ea Changed theme order to default to classic 2022-02-17 21:46:51 +00:00
Ben Arc e9065833c0 Fixed themes, added bitcoin theme 2022-02-17 21:34:36 +00:00
Tiago vasconcelos 251ecb8f1d url_for params 2022-02-17 21:01:14 +00:00
calleandGitHub c626e12fa3 LNDHhub for fastapi with LIMIT (#499)
* add limit to get_payments

* add limit to lndhub

* fix defaults

* default memo

* add offset cluase

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

* fix GRPC env entry

* example config entry

* add pycryptodomex to requirements

* documentation

* Added pycryptodomex to pip file

Co-authored-by: Ben Arc <ben@arc.wales>
2022-02-14 17:54:05 +01:00
Ben Arc 0367ee85a7 removed dense from lower buttons 2022-02-14 15:51:45 +00:00
Ben Arc 0aef9d4d9a Made dense to save space 2022-02-14 15:48:56 +00:00
Ben Arc 8967011cbe Merge branch 'mobileui' into main 2022-02-14 15:38:17 +00:00
Ben Arc 682226c4b6 chnaged button names 2022-02-14 15:36:10 +00:00
Ben Arc c7e501ac08 Adds lower buttons to wallet UI for smaller screens 2022-02-14 15:29:10 +00:00
calleandGitHub 8d7302e4cd fix bleskomat test (#523) 2022-02-14 13:49:15 +01:00
callebtc 09b86605dd fix bleskomat tests 2022-02-14 13:42:43 +01:00
orangesurfandGitHub 9e8376b3fb Remove suggestion to switch to FastAPI branch (#520)
Branch does not exist
2022-02-14 13:38:22 +01:00
ArcandGitHub 1a492cbfb8 Update installation.md 2022-02-14 11:42:19 +00:00
ArcandGitHub 155c32504a Update api.py 2022-02-14 11:31:42 +00:00
ArcandGitHub 8c923ad653 Update README.md 2022-02-14 09:23:04 +00:00
ArcandGitHub 25b9014c3d Update README.md 2022-02-14 09:22:23 +00:00
ArcandGitHub 9807e7d646 Merge pull request #518 from talvasconcelos/fix/disapearingAdminExt
Disapearing Extensions in Admin Only
2022-02-11 11:17:55 +00:00
ArcandGitHub 728ab517f5 Merge branch 'main' into fix/disapearingAdminExt 2022-02-11 11:17:37 +00:00
benarc f1fb569e2b lnurlpayout fix 2022-02-11 10:03:26 +00:00
Tiago vasconcelos 6d58027147 remove print 2022-02-10 17:06:15 +00:00
Tiago vasconcelos 4a35f84e95 fix admin default false 2022-02-10 16:44:47 +00:00
ArcandGitHub f7b37fb843 Merge pull request #498 from pseudozach/main
fix livestream startup issue
2022-02-10 15:24:05 +00:00
ArcandGitHub 699bdd9a06 Merge pull request #497 from bitkarrot/main
fix curl example paths in API docs on LNURLw extension
2022-02-10 15:21:28 +00:00
benarc b5aad94ecd rolled back admin fix 2022-02-10 13:55:45 +00:00
benarc 0f79213b0f Tals admin extension fix 2022-02-10 10:09:44 +00:00
ArcandGitHub c1654a21d4 Merge pull request #515 from lnbits/revert-513-fix/disapearingAdminExt
Revert "fix disapearing admin extension"
2022-02-10 09:56:15 +00:00
ArcandGitHub da6a2e772b Revert "fix disapearing admin extension" 2022-02-10 09:56:02 +00:00
ArcandGitHub 1626cd2b4b Merge pull request #513 from talvasconcelos/fix/disapearingAdminExt
fix disapearing admin extension
2022-02-10 09:55:14 +00:00
Tiago vasconcelos d1d62583c4 fix for whitespace in admin users list 2022-02-09 11:16:13 +00:00
Tiago vasconcelos 4c18eb0b34 fix disapearing admin extension 2022-02-09 10:57:24 +00:00
ArcandGitHub 35bb26b6fd Merge pull request #512 from lnbits/adminextensions
Adds admin extensions
2022-02-07 21:32:17 +00:00
benarc 2c853e7a52 slight bug 2022-02-07 21:28:48 +00:00
benarc 6bc1474c19 Organised .env 2022-02-07 20:48:48 +00:00
benarc 4b11342f0c works 2022-02-07 20:43:47 +00:00
pseudozach b00a39a7ce fix livestream startup issue 2022-01-24 15:58:51 -08:00
BitkarrotandGitHub b4dc07170f fix curl example paths 2022-01-24 14:51:39 -08:00
33 changed files with 524 additions and 231 deletions
+10 -4
View File
@@ -5,11 +5,15 @@ QUART_DEBUG=true
HOST=127.0.0.1 HOST=127.0.0.1
PORT=5000 PORT=5000
LNBITS_ALLOWED_USERS="" LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS="" LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER # Database: to use SQLite, specify LNBITS_DATA_FOLDER
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... # to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... # to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
@@ -18,8 +22,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="./data" LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" # LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
# disable selected extensions, or use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
LNBITS_FORCE_HTTPS=true LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0" LNBITS_SERVICE_FEE="0.0"
@@ -28,7 +30,7 @@ LNBITS_SITE_TITLE="LNbits"
LNBITS_SITE_TAGLINE="free and open-source lightning wallet" LNBITS_SITE_TAGLINE="free and open-source lightning wallet"
LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'"
# Choose from mint, flamingo, salvador, autumn, monochrome, classic # Choose from mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="mint, flamingo, classic, autumn, monochrome, salvador" LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
@@ -53,11 +55,15 @@ LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009 LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_GRPC_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LndRestWallet # LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/ LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" LND_REST_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_REST_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LNPayWallet # LNPayWallet
LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/ LNPAY_API_ENDPOINT=https://api.lnpay.co/v1/
+1
View File
@@ -30,6 +30,7 @@ sse-starlette = "*"
jinja2 = "3.0.1" jinja2 = "3.0.1"
pyngrok = "*" pyngrok = "*"
secp256k1 = "*" secp256k1 = "*"
pycryptodomex = "*"
[dev-packages] [dev-packages]
black = "==20.8b1" black = "==20.8b1"
Generated
+97 -67
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "bd78144379115a1566549f5f2d7dba7a96539fb5893b3999e61bc13c6847827e" "sha256": "3e19364434fd2db3748162ccc1f3b6bddcf7a382473069d15cee6eda5e07eef1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -146,11 +146,11 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==2.0.11" "version": "==2.0.12"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@@ -201,11 +201,11 @@
}, },
"httpcore": { "httpcore": {
"hashes": [ "hashes": [
"sha256:2621ee769d0236574df51b305c5f4c69ca8f0c7b215221ad247b1ee42a9a9de1", "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade",
"sha256:435ab519628a6e2393f67812dea3ca5c6ad23b457412cd119295d9f906d96a2b" "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.14.5" "version": "==0.14.7"
}, },
"httptools": { "httptools": {
"hashes": [ "hashes": [
@@ -427,6 +427,39 @@
], ],
"version": "==2.21" "version": "==2.21"
}, },
"pycryptodomex": {
"hashes": [
"sha256:1ca8e1b4c62038bb2da55451385246f51f412c5f5eabd64812c01766a5989b4a",
"sha256:298c00ea41a81a491d5b244d295d18369e5aac4b61b77b2de5b249ca61cd6659",
"sha256:2aa887683eee493e015545bd69d3d21ac8d5ad582674ec98f4af84511e353e45",
"sha256:2ce76ed0081fd6ac8c74edc75b9d14eca2064173af79843c24fa62573263c1f2",
"sha256:3da13c2535b7aea94cc2a6d1b1b37746814c74b6e80790daddd55ca5c120a489",
"sha256:406ec8cfe0c098fadb18d597dc2ee6de4428d640c0ccafa453f3d9b2e58d29e2",
"sha256:4d0db8df9ffae36f416897ad184608d9d7a8c2b46c4612c6bc759b26c073f750",
"sha256:530756d2faa40af4c1f74123e1d889bd07feae45bac2fd32f259a35f7aa74151",
"sha256:77931df40bb5ce5e13f4de2bfc982b2ddc0198971fbd947776c8bb5050896eb2",
"sha256:797a36bd1f69df9e2798e33edb4bd04e5a30478efc08f9428c087f17f65a7045",
"sha256:8085bd0ad2034352eee4d4f3e2da985c2749cb7344b939f4d95ead38c2520859",
"sha256:8536bc08d130cae6dcba1ea689f2913dfd332d06113904d171f2f56da6228e89",
"sha256:a4d412eba5679ede84b41dbe48b1bed8f33131ab9db06c238a235334733acc5e",
"sha256:aebecde2adc4a6847094d3bd6a8a9538ef3438a5ea84ac1983fcb167db614461",
"sha256:b276cc4deb4a80f9dfd47a41ebb464b1fe91efd8b1b8620cf5ccf8b824b850d6",
"sha256:b5a185ae79f899b01ca49f365bdf15a45d78d9856f09b0de1a41b92afce1a07f",
"sha256:c4d8977ccda886d88dc3ca789de2f1adc714df912ff3934b3d0a3f3d777deafb",
"sha256:c5dd3ffa663c982d7f1be9eb494a8924f6d40e2e2f7d1d27384cfab1b2ac0662",
"sha256:ca88f2f7020002638276439a01ffbb0355634907d1aa5ca91f3dc0c2e44e8f3b",
"sha256:d2cce1c82a7845d7e2e8a0956c6b7ed3f1661c9acf18eb120fc71e098ab5c6fe",
"sha256:d709572d64825d8d59ea112e11cc7faf6007f294e9951324b7574af4251e4de8",
"sha256:da8db8374295fb532b4b0c467e66800ef17d100e4d5faa2bbbd6df35502da125",
"sha256:e36c7e3b5382cd5669cf199c4a04a0279a43b2a3bdd77627e9b89778ac9ec08c",
"sha256:e95a4a6c54d27a84a4624d2af8bb9ee178111604653194ca6880c98dcad92f48",
"sha256:ee835def05622e0c8b1435a906491760a43d0c462f065ec9143ec4b8d79f8bff",
"sha256:f75009715dcf4a3d680c2338ab19dac5498f8121173a929872950f4fb3a48fbf",
"sha256:f8524b8bc89470cec7ac51734907818d3620fb1637f8f8b542d650ebec42a126"
],
"index": "pypi",
"version": "==3.14.1"
},
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3", "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3",
@@ -684,22 +717,22 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
}, },
"uvicorn": { "uvicorn": {
"extras": [ "extras": [
"standard" "standard"
], ],
"hashes": [ "hashes": [
"sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73", "sha256:25850bbc86195a71a6477b3e4b3b7b4c861fb687fb96912972ce5324472b1011",
"sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41" "sha256:e85872d84fb651cccc4c5d2a71cf7ead055b8fb4d8f1e78e36092282c0cf2aec"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.17.1" "version": "==0.17.4"
}, },
"uvloop": { "uvloop": {
"hashes": [ "hashes": [
@@ -819,53 +852,50 @@
"toml" "toml"
], ],
"hashes": [ "hashes": [
"sha256:012157499ec4f135fc36cd2177e3d1a1840af9b236cbe80e9a5ccfc83d912a69", "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c",
"sha256:0a34d313105cdd0d3644c56df2d743fe467270d6ab93b5d4a347eb9fec8924d6", "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0",
"sha256:11e61c5548ecf74ea1f8b059730b049871f0e32b74f88bd0d670c20c819ad749", "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554",
"sha256:152cc2624381df4e4e604e21bd8e95eb8059535f7b768c1fb8b8ae0b26f47ab0", "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb",
"sha256:1b4285fde5286b946835a1a53bba3ad41ef74285ba9e8013e14b5ea93deaeafc", "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2",
"sha256:27a94db5dc098c25048b0aca155f5fac674f2cf1b1736c5272ba28ead2fc267e", "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b",
"sha256:27ac7cb84538e278e07569ceaaa6f807a029dc194b1c819a9820b9bb5dbf63ab", "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8",
"sha256:2a491e159294d756e7fc8462f98175e2d2225e4dbe062cca7d3e0d5a75ba6260", "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba",
"sha256:2bc85664b06ba42d14bb74d6ddf19d8bfc520cb660561d2d9ce5786ae72f71b5", "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734",
"sha256:32168001f33025fd756884d56d01adebb34e6c8c0b3395ca8584cdcee9c7c9d2", "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2",
"sha256:3c4ce3b647bd1792d4394f5690d9df6dc035b00bcdbc5595099c01282a59ae01", "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f",
"sha256:433b99f7b0613bdcdc0b00cc3d39ed6d756797e3b078d2c43f8a38288520aec6", "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0",
"sha256:4578728c36de2801c1deb1c6b760d31883e62e33f33c7ba8f982e609dc95167d", "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1",
"sha256:509c68c3e2015022aeda03b003dd68fa19987cdcf64e9d4edc98db41cfc45d30", "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd",
"sha256:51372e24b1f7143ee2df6b45cff6a721f3abe93b1e506196f3ffa4155c2497f7", "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687",
"sha256:5d008e0f67ac800b0ca04d7914b8501312c8c6c00ad8c7ba17754609fae1231a", "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1",
"sha256:649df3641eb351cdfd0d5533c92fc9df507b6b2bf48a7ef8c71ab63cbc7b5c3c", "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c",
"sha256:6e78b1e25e5c5695dea012be473e442f7094d066925604be20b30713dbd47f89", "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa",
"sha256:72d9d186508325a456475dd05b1756f9a204c7086b07fffb227ef8cee03b1dc2", "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8",
"sha256:7d82c610a2e10372e128023c5baf9ce3d270f3029fe7274ff5bc2897c68f1318", "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38",
"sha256:7ee317486593193e066fc5e98ac0ce712178c21529a85c07b7cb978171f25d53", "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8",
"sha256:7eed8459a2b81848cafb3280b39d7d49950d5f98e403677941c752e7e7ee47cb", "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167",
"sha256:823f9325283dc9565ba0aa2d240471a93ca8999861779b2b6c7aded45b58ee0f", "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27",
"sha256:85c5fc9029043cf8b07f73fbb0a7ab6d3b717510c3b5642b77058ea55d7cacde", "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145",
"sha256:86c91c511853dfda81c2cf2360502cb72783f4b7cebabef27869f00cbe1db07d", "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa",
"sha256:8e0c3525b1a182c8ffc9bca7e56b521e0c2b8b3e82f033c8e16d6d721f1b54d6", "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a",
"sha256:987a84ff98a309994ca77ed3cc4b92424f824278e48e4bf7d1bb79a63cfe2099", "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed",
"sha256:9ed3244b415725f08ca3bdf02ed681089fd95e9465099a21c8e2d9c5d6ca2606", "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793",
"sha256:a189036c50dcd56100746139a459f0d27540fef95b09aba03e786540b8feaa5f", "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4",
"sha256:a4748349734110fd32d46ff8897b561e6300d8989a494ad5a0a2e4f0ca974fc7", "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217",
"sha256:a5d79c9af3f410a2b5acad91258b4ae179ee9c83897eb9de69151b179b0227f5", "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e",
"sha256:a7596aa2f2b8fa5604129cfc9a27ad9beec0a96f18078cb424d029fdd707468d", "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6",
"sha256:ab4fc4b866b279740e0d917402f0e9a08683e002f43fa408e9655818ed392196", "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d",
"sha256:bde4aeabc0d1b2e52c4036c54440b1ad05beeca8113f47aceb4998bb7471e2c2", "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320",
"sha256:c72bb4679283c6737f452eeb9b2a0e570acaef2197ad255fb20162adc80bea76", "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f",
"sha256:c8582e9280f8d0f38114fe95a92ae8d0790b56b099d728cc4f8a2e14b1c4a18c", "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce",
"sha256:ca29c352389ea27a24c79acd117abdd8a865c6eb01576b6f0990cd9a4e9c9f48", "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975",
"sha256:ce443a3e6df90d692c38762f108fc4c88314bf477689f04de76b3f252e7a351c", "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10",
"sha256:d1675db48490e5fa0b300f6329ecb8a9a37c29b9ab64fa9c964d34111788ca2d", "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525",
"sha256:da1a428bdbe71f9a8c270c7baab29e9552ac9d0e0cba5e7e9a4c9ee6465d258d", "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda",
"sha256:e4ff163602c5c77e7bb4ea81ba5d3b793b4419f8acd296aae149370902cf4e92", "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"
"sha256:e67ccd53da5958ea1ec833a160b96357f90859c220a00150de011b787c27b98d",
"sha256:e8071e7d9ba9f457fc674afc3de054450be2c9b195c470147fbbc082468d8ff7",
"sha256:fff16a30fdf57b214778eff86391301c4509e327a65b877862f7c929f10a4253"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==6.3" "version": "==6.3.1"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@@ -948,11 +978,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.5" "version": "==7.0.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@@ -1051,10 +1081,10 @@
}, },
"tomli": { "tomli": {
"hashes": [ "hashes": [
"sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
], ],
"version": "==2.0.0" "version": "==2.0.1"
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
@@ -1088,11 +1118,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.0.1" "version": "==4.1.1"
} }
} }
} }
-3
View File
@@ -1,9 +1,6 @@
LNbits LNbits
====== ======
[![github-tests-badge]][github-tests]
[![github-mypy-badge]][github-mypy]
[![codecov-badge]][codecov]
[![license-badge]](LICENSE) [![license-badge]](LICENSE)
[![docs-badge]][docs] [![docs-badge]][docs]
+6 -3
View File
@@ -13,9 +13,9 @@ LNbits uses [Pipenv][pipenv] to manage Python packages.
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
pipenv shell pipenv shell
# pipenv --python 3.8 shell (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev pipenv install --dev
# pipenv --python 3.8 install --dev (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 install --dev (if you wish to use a version of Python higher than 3.7)
# If any of the modules fails to install, try checking and upgrading your setupTool module # If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools # pip install -U setuptools
@@ -41,4 +41,7 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
**Note**: We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy, if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). **Notes**:
* We reccomend using <a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian">Caddy</a> for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/).
* <a href="https://linuxize.com/post/how-to-use-linux-screen/#starting-linux-screen">Screen</a> works well if you want LNbits to continue running when you close your terminal session.
-1
View File
@@ -28,7 +28,6 @@ Download this repo and install the dependencies:
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work # ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
# for now you'll need to `git checkout FastAPI`
python3 -m venv venv python3 -m venv venv
./venv/bin/pip install -r requirements.txt ./venv/bin/pip install -r requirements.txt
cp .env.example .env cp .env.example .env
+10 -2
View File
@@ -36,16 +36,24 @@ Using this wallet requires the installation of the `grpcio` and `protobuf` Pytho
- `LND_GRPC_ENDPOINT`: ip_address - `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port - `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert - `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon - `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
You can also use an AES-encrypted macaroon (more info) instead by using
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroon.py`.
### LND (REST) ### LND (REST)
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: ip_address - `LND_REST_ENDPOINT`: ip_address
- `LND_REST_CERT`: /file/path/tls.cert - `LND_REST_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon - `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex
or
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
### LNbits ### LNbits
+16 -2
View File
@@ -6,7 +6,7 @@ from urllib.parse import urlparse
from lnbits import bolt11 from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db from . import db
from .models import User, Wallet, Payment, BalanceCheck from .models import User, Wallet, Payment, BalanceCheck
@@ -61,6 +61,9 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
email=user["email"], email=user["email"],
extensions=[e[0] for e in extensions], extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets], wallets=[Wallet(**w) for w in wallets],
admin=user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
if LNBITS_ADMIN_USERS
else False,
) )
@@ -216,6 +219,8 @@ async def get_payments(
incoming: bool = False, incoming: bool = False,
since: Optional[int] = None, since: Optional[int] = None,
exclude_uncheckable: bool = False, exclude_uncheckable: bool = False,
limit: Optional[int] = None,
offset: Optional[int] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> List[Payment]: ) -> List[Payment]:
""" """
@@ -260,6 +265,15 @@ async def get_payments(
clause.append("checking_id NOT LIKE 'temp_%'") clause.append("checking_id NOT LIKE 'temp_%'")
clause.append("checking_id NOT LIKE 'internal_%'") clause.append("checking_id NOT LIKE 'internal_%'")
limit_clause = f"LIMIT {limit}" if type(limit) == int and limit > 0 else ""
offset_clause = f"OFFSET {offset}" if type(offset) == int and offset > 0 else ""
# combine limit and offset clauses
limit_offset_clause = (
f"{limit_clause} {offset_clause}"
if limit_clause and offset_clause
else limit_clause or offset_clause
)
where = "" where = ""
if clause: if clause:
where = f"WHERE {' AND '.join(clause)}" where = f"WHERE {' AND '.join(clause)}"
@@ -270,10 +284,10 @@ async def get_payments(
FROM apipayments FROM apipayments
{where} {where}
ORDER BY time DESC ORDER BY time DESC
{limit_offset_clause}
""", """,
tuple(args), tuple(args),
) )
return [Payment.from_row(row) for row in rows] return [Payment.from_row(row) for row in rows]
+1 -1
View File
@@ -61,7 +61,7 @@
<code <code
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, >curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
"amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook": "amount": &lt;int&gt;, "memo": &lt;string&gt;, "webhook":
&lt;url:string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H &lt;url:string&gt;, "unit": &lt;string&gt;}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
"Content-type: application/json"</code "Content-type: application/json"</code
> >
</q-card-section> </q-card-section>
+1 -1
View File
@@ -11,7 +11,7 @@
color="primary" color="primary"
@click="processing" @click="processing"
type="a" type="a"
href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}" href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}"
> >
Press to claim bitcoin Press to claim bitcoin
</q-btn> </q-btn>
+23 -5
View File
@@ -62,7 +62,7 @@
</q-btn> </q-btn>
</h3> </h3>
</q-card-section> </q-card-section>
<div class="row q-pb-md q-px-md q-col-gutter-md"> <div class="row q-pb-md q-px-md q-col-gutter-md gt-sm">
<div class="col"> <div class="col">
<q-btn <q-btn
unelevated unelevated
@@ -417,9 +417,11 @@
filled filled
dense dense
v-model.number="receive.data.amount" v-model.number="receive.data.amount"
type="number" :label="'Amount (' + receive.unit + ') *'"
label="Amount ({{LNBITS_DENOMINATION}}) *" :mask="receive.unit != 'sat' ? '#.##' : '#'"
:step="receive.unit != 'sat' ? '0.001' : '1'" fill-mask="0"
reverse-fill-mask
:step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]" :min="receive.minMax[0]"
:max="receive.minMax[1]" :max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed" :readonly="receive.lnurl && receive.lnurl.fixed"
@@ -437,7 +439,7 @@
<q-btn <q-btn
unelevated unelevated
color="primary" color="primary"
:disable="receive.data.memo == null || receive.data.amount == null || receive.data.amount <= 0" :disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit" type="submit"
> >
<span v-if="receive.lnurl"> <span v-if="receive.lnurl">
@@ -662,7 +664,23 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0"
indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs>
{% if service_fee > 0 %} {% if service_fee > 0 %}
<div ref="disclaimer"></div> <div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show"> <q-dialog v-model="disclaimerDialog.show">
+10 -9
View File
@@ -31,6 +31,7 @@ from lnbits.utils.exchange_rates import (
fiat_amount_as_satoshis, fiat_amount_as_satoshis,
satoshis_amount_as_fiat, satoshis_amount_as_fiat,
) )
from lnbits.settings import LNBITS_SITE_TITLE
from .. import core_app, db from .. import core_app, db
from ..crud import ( from ..crud import (
@@ -71,7 +72,7 @@ async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
async def api_update_balance( async def api_update_balance(
amount: int, wallet: WalletTypeInfo = Depends(get_key_type) amount: int, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
if LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS: if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user" status_code=HTTPStatus.FORBIDDEN, detail="Not an admin user"
) )
@@ -123,7 +124,7 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
out: Optional[bool] = True out: Optional[bool] = True
amount: int = Query(None, ge=1) amount: float = Query(None, ge=0)
memo: str = None memo: str = None
unit: Optional[str] = "sat" unit: Optional[str] = "sat"
description_hash: Optional[str] = None description_hash: Optional[str] = None
@@ -140,9 +141,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
memo = "" memo = ""
else: else:
description_hash = b"" description_hash = b""
memo = data.memo memo = data.memo or LNBITS_SITE_TITLE
if data.unit == "sat": if data.unit == "sat":
amount = data.amount amount = int(data.amount)
else: else:
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit) price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats amount = price_in_sats
@@ -292,11 +293,11 @@ async def api_payments_pay_lnurl(
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.", detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
) )
# if invoice.description_hash != data.description_hash: # if invoice.description_hash != data.description_hash:
# raise HTTPException( # raise HTTPException(
# status_code=HTTPStatus.BAD_REQUEST, # status_code=HTTPStatus.BAD_REQUEST,
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.", # detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
# ) # )
extra = {} extra = {}
+2 -2
View File
@@ -15,8 +15,8 @@ from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer, url_for from lnbits.helpers import template_renderer, url_for
from lnbits.settings import ( from lnbits.settings import (
LNBITS_ALLOWED_USERS,
LNBITS_ADMIN_USERS, LNBITS_ADMIN_USERS,
LNBITS_ALLOWED_USERS,
LNBITS_SITE_TITLE, LNBITS_SITE_TITLE,
SERVICE_FEE, SERVICE_FEE,
) )
@@ -226,7 +226,7 @@ async def lnurl_balance_notify(request: Request, service: str):
redeem_lnurl_withdraw(bc.wallet, bc.url) redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse) @core_html_routes.get("/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet")
async def lnurlwallet(request: Request): async def lnurlwallet(request: Request):
async with db.connect() as conn: async with db.connect() as conn:
account = await create_account(conn=conn) account = await create_account(conn=conn)
+10 -3
View File
@@ -13,7 +13,7 @@ from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet from lnbits.core.models import User, Wallet
from lnbits.requestvars import g from lnbits.requestvars import g
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
class KeyChecker(SecurityBase): class KeyChecker(SecurityBase):
@@ -122,6 +122,7 @@ async def get_key_type(
# 0: admin # 0: admin
# 1: invoice # 1: invoice
# 2: invalid # 2: invalid
pathname = r['path'].split('/')[1]
if not api_key_header and not api_key_query: if not api_key_header and not api_key_query:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@@ -131,7 +132,10 @@ async def get_key_type(
try: try:
checker = WalletAdminKeyChecker(api_key=token) checker = WalletAdminKeyChecker(api_key=token)
await checker.__call__(r) await checker.__call__(r)
return WalletTypeInfo(0, checker.wallet) wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet
except HTTPException as e: except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST: if e.status_code == HTTPStatus.BAD_REQUEST:
raise raise
@@ -143,7 +147,10 @@ async def get_key_type(
try: try:
checker = WalletInvoiceKeyChecker(api_key=token) checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r) await checker.__call__(r)
return WalletTypeInfo(1, checker.wallet) wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet
except HTTPException as e: except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST: if e.status_code == HTTPStatus.BAD_REQUEST:
raise raise
+1 -1
View File
@@ -30,6 +30,6 @@ from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
def lnticket_start(): def livestream_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
+1 -1
View File
@@ -1,5 +1,5 @@
from http import HTTPStatus from http import HTTPStatus
from mmap import MAP_DENYWRITE # from mmap import MAP_DENYWRITE
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.params import Query from fastapi.params import Query
+23 -8
View File
@@ -1,4 +1,6 @@
import time import time
import asyncio
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from http import HTTPStatus from http import HTTPStatus
@@ -11,7 +13,7 @@ from lnbits import bolt11
from lnbits.core.crud import delete_expired_invoices, get_payments from lnbits.core.crud import delete_expired_invoices, get_payments
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice
from lnbits.decorators import WalletTypeInfo from lnbits.decorators import WalletTypeInfo
from lnbits.settings import WALLET from lnbits.settings import WALLET, LNBITS_SITE_TITLE
from . import lndhub_ext from . import lndhub_ext
from .decorators import check_wallet, require_admin_key from .decorators import check_wallet, require_admin_key
@@ -55,14 +57,13 @@ async def lndhub_addinvoice(
_, pr = await create_invoice( _, pr = await create_invoice(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
amount=int(data.amt), amount=int(data.amt),
memo=data.memo or "received sats", memo=data.memo or LNBITS_SITE_TITLE,
extra={"tag": "lndhub"}, extra={"tag": "lndhub"},
) )
except: except:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice" status_code=HTTPStatus.NOT_FOUND, detail="Failed to create invoice"
) )
invoice = bolt11.decode(pr) invoice = bolt11.decode(pr)
return { return {
"pay_req": pr, "pay_req": pr,
@@ -116,7 +117,9 @@ async def lndhub_balance(
@lndhub_ext.get("/ext/gettxs") @lndhub_ext.get("/ext/gettxs")
async def lndhub_gettxs( async def lndhub_gettxs(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
for payment in await get_payments( for payment in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
@@ -124,11 +127,15 @@ async def lndhub_gettxs(
pending=True, pending=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await payment.set_pending( await payment.set_pending(
(await WALLET.get_payment_status(payment.checking_id)).pending (await WALLET.get_payment_status(payment.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
"payment_preimage": payment.preimage, "payment_preimage": payment.preimage,
@@ -148,28 +155,34 @@ async def lndhub_gettxs(
complete=True, complete=True,
outgoing=True, outgoing=True,
incoming=False, incoming=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]
@lndhub_ext.get("/ext/getuserinvoices") @lndhub_ext.get("/ext/getuserinvoices")
async def lndhub_getuserinvoices( async def lndhub_getuserinvoices(
wallet: WalletTypeInfo = Depends(check_wallet), limit: int = Query(0, ge=0, lt=200) wallet: WalletTypeInfo = Depends(check_wallet),
limit: int = Query(20, ge=1, le=20),
offset: int = Query(0, ge=0),
): ):
await delete_expired_invoices()
for invoice in await get_payments( for invoice in await get_payments(
wallet_id=wallet.wallet.id, wallet_id=wallet.wallet.id,
complete=False, complete=False,
pending=True, pending=True,
outgoing=False, outgoing=False,
incoming=True, incoming=True,
limit=limit,
offset=offset,
exclude_uncheckable=True, exclude_uncheckable=True,
): ):
await invoice.set_pending( await invoice.set_pending(
(await WALLET.get_invoice_status(invoice.checking_id)).pending (await WALLET.get_invoice_status(invoice.checking_id)).pending
) )
await asyncio.sleep(0.1)
return [ return [
{ {
@@ -192,8 +205,10 @@ async def lndhub_getuserinvoices(
complete=True, complete=True,
incoming=True, incoming=True,
outgoing=False, outgoing=False,
limit=limit,
offset=offset,
) )
)[:limit] )
) )
] ]
+22 -11
View File
@@ -1,15 +1,17 @@
import asyncio import asyncio
import json from http import HTTPStatus
import httpx import httpx
from starlette.exceptions import HTTPException
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.models import Payment
from lnbits.tasks import register_invoice_listener
from lnbits.core.views.api import api_wallet
from lnbits.core.crud import get_wallet from lnbits.core.crud import get_wallet
from lnbits.core.views.api import api_payment, api_payments_decode, pay_invoice from lnbits.core.models import Payment
from lnbits.core.services import pay_invoice
from lnbits.core.views.api import api_payments_decode
from lnbits.tasks import register_invoice_listener
from .crud import get_lnurlpayout, get_lnurlpayout_from_wallet from .crud import get_lnurlpayout_from_wallet
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
@@ -25,16 +27,16 @@ async def on_invoice_paid(payment: Payment) -> None:
try: try:
# Check its got a payout associated with it # Check its got a payout associated with it
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id) lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
print("LNURLpayout", lnurlpayout_link)
if lnurlpayout_link: if lnurlpayout_link:
# Check the wallet balance is more than the threshold # Check the wallet balance is more than the threshold
wallet = await get_wallet(lnurlpayout_link.wallet) wallet = await get_wallet(lnurlpayout_link.wallet)
if wallet.balance < lnurlpayout_link.threshold + ( threshold = lnurlpayout_link.threshold + (lnurlpayout_link.threshold * 0.02)
lnurlpayout_link.threshold * 0.02
):
return
if wallet.balance < threshold:
return
# Get the invoice from the LNURL to pay # Get the invoice from the LNURL to pay
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
@@ -43,6 +45,7 @@ async def on_invoice_paid(payment: Payment) -> None:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken" status_code=HTTPStatus.FORBIDDEN, detail="LNURL broken"
) )
try: try:
r = await client.get(str(url["domain"]), timeout=40) r = await client.get(str(url["domain"]), timeout=40)
res = r.json() res = r.json()
@@ -56,6 +59,12 @@ async def on_invoice_paid(payment: Payment) -> None:
timeout=40, timeout=40,
) )
res = r.json() res = r.json()
if hasattr(res, "status") and res["status"] == "ERROR":
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail=res["reason"],
)
try: try:
await pay_invoice( await pay_invoice(
wallet_id=payment.wallet_id, wallet_id=payment.wallet_id,
@@ -65,7 +74,9 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
except: except:
pass pass
except:
except Exception as e:
print("ERROR", str(e))
return return
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError):
return return
+20 -9
View File
@@ -4,7 +4,8 @@ from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user, get_payments from lnbits.core.crud import get_payments, get_user
from lnbits.core.models import Payment
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment, api_payments_decode from lnbits.core.views.api import api_payment, api_payments_decode
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
@@ -14,10 +15,10 @@ from .crud import (
create_lnurlpayout, create_lnurlpayout,
delete_lnurlpayout, delete_lnurlpayout,
get_lnurlpayout, get_lnurlpayout,
get_lnurlpayouts,
get_lnurlpayout_from_wallet, get_lnurlpayout_from_wallet,
get_lnurlpayouts,
) )
from .models import lnurlpayout, CreateLnurlPayoutData from .models import CreateLnurlPayoutData, lnurlpayout
from .tasks import on_invoice_paid from .tasks import on_invoice_paid
@@ -87,14 +88,24 @@ async def api_lnurlpayout_check(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type) lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
): ):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id) lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
payments = await get_payments( ## THIS
wallet_id=lnurlpayout.wallet, mock_payment = Payment(
complete=True, checking_id="mock",
pending=False, pending=False,
outgoing=True, amount=1,
incoming=True, fee=1,
time=0000,
bolt11="mock",
preimage="mock",
payment_hash="mock",
wallet_id=lnurlpayout.wallet,
) )
result = await on_invoice_paid(payments[0]) ## INSTEAD OF THIS
# payments = await get_payments(
# wallet_id=lnurlpayout.wallet, complete=True, pending=False, outgoing=True, incoming=True
# )
result = await on_invoice_paid(mock_payment)
return return
@@ -77,8 +77,8 @@
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
userAmount: {{ paywall.amount }}, userAmount: '{{ paywall.amount }}',
paywallAmount: {{ paywall.amount }}, paywallAmount: '{{ paywall.amount }}',
paymentReq: null, paymentReq: null,
redirectUrl: null, redirectUrl: null,
paymentDialog: { paymentDialog: {
@@ -89,7 +89,9 @@
}, },
computed: { computed: {
amount: function () { amount: function () {
return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount return this.paywallAmount > this.userAmount
? this.paywallAmount
: this.userAmount
} }
}, },
methods: { methods: {
@@ -102,48 +104,55 @@
}, },
createInvoice: function () { createInvoice: function () {
var self = this var self = this
console.log(this.amount) LNbits.api
axios .request(
.post( 'POST',
'/paywall/api/v1/paywalls/{{ paywall.id }}/invoice', '/paywall/api/v1/paywalls/invoice/{{ paywall.id }}',
{amount: self.amount} 'filler',
{
amount: self.amount
}
) )
.then(function (response) { .then(function (response) {
self.paymentReq = response.data.payment_request.toUpperCase() if (response.data) {
self.paymentReq = response.data.payment_request.toUpperCase()
self.paymentDialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
self.paymentDialog.dismissMsg = self.$q.notify({ self.paymentDialog.checker = setInterval(function () {
timeout: 0, LNbits.api
message: 'Waiting for payment...' .request(
}) 'POST',
'/paywall/api/v1/paywalls/check_invoice/{{ paywall.id }}',
self.paymentDialog.checker = setInterval(function () { 'filler',
axios {payment_hash: response.data.payment_hash}
.post( )
'/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', .then(function (response) {
{payment_hash: response.data.payment_hash} if (response.data) {
) if (response.data.paid) {
.then(function (res) { self.cancelPayment()
if (res.data.paid) { self.redirectUrl = response.data.url
self.cancelPayment() if (response.data.remembers) {
self.redirectUrl = res.data.url self.$q.localStorage.set(
if (res.data.remembers) { 'lnbits.paywall.{{ paywall.id }}',
self.$q.localStorage.set( response.data.url
'lnbits.paywall.{{ paywall.id }}', )
res.data.url }
) self.$q.notify({
type: 'positive',
message: 'Payment received!',
icon: null
})
}
} }
})
self.$q.notify({ .catch(function (error) {
type: 'positive', LNbits.utils.notifyApiError(error)
message: 'Payment received!', })
icon: null }, 2000)
}) }
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
}, 2000)
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
+4 -9
View File
@@ -52,20 +52,17 @@ async def api_paywall_delete(
raise HTTPException(status_code=HTTPStatus.NO_CONTENT) raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/invoice") @paywall_ext.post("/api/v1/paywalls/invoice/{paywall_id}")
async def api_paywall_create_invoice( async def api_paywall_create_invoice(
paywall_id,
data: CreatePaywallInvoice, data: CreatePaywallInvoice,
wallet: WalletTypeInfo = Depends(get_key_type), paywall_id: str = Query(None)
): ):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
if data.amount < paywall.amount: if data.amount < paywall.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Minimum amount is {paywall.amount} sat.", detail=f"Minimum amount is {paywall.amount} sat.",
) )
try: try:
amount = data.amount if data.amount > paywall.amount else paywall.amount amount = data.amount if data.amount > paywall.amount else paywall.amount
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
@@ -80,15 +77,14 @@ async def api_paywall_create_invoice(
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}
@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice") @paywall_ext.post("/api/v1/paywalls/check_invoice/{paywall_id}")
async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id): async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id: str = Query(None)):
paywall = await get_paywall(paywall_id) paywall = await get_paywall(paywall_id)
payment_hash = data.payment_hash payment_hash = data.payment_hash
if not paywall: if not paywall:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Paywall does not exist."
) )
try: try:
status = await check_invoice_status(paywall.wallet, payment_hash) status = await check_invoice_status(paywall.wallet, payment_hash)
is_paid = not status.pending is_paid = not status.pending
@@ -101,5 +97,4 @@ async def api_paywal_check_invoice(data: CheckPaywallInvoice, paywall_id):
await payment.set_pending(False) await payment.set_pending(False)
return {"paid": True, "url": paywall.url, "remembers": paywall.remembers} return {"paid": True, "url": paywall.url, "remembers": paywall.remembers}
return {"paid": False} return {"paid": False}
@@ -22,7 +22,7 @@
<code>[&lt;withdraw_link_object&gt;, ...]</code> <code>[&lt;withdraw_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/links -H "X-Api-Key: {{ >curl -X GET {{ request.base_url }}withdraw/api/v1/links -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -49,7 +49,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -H >curl -X GET {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -H
"X-Api-Key: {{ user.wallets[0].inkey }}" "X-Api-Key: {{ user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -78,7 +78,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}api/v1/links -d '{"title": >curl -X POST {{ request.base_url }}withdraw/api/v1/links -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;, &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -114,7 +114,7 @@
<code>{"lnurl": &lt;string&gt;}</code> <code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X PUT {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; -d >curl -X PUT {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt; -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;, '{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;, "max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H "wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
@@ -142,7 +142,7 @@
<code></code> <code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url }}api/v1/links/&lt;withdraw_id&gt; >curl -X DELETE {{ request.base_url }}withdraw/api/v1/links/&lt;withdraw_id&gt;
-H "X-Api-Key: {{ user.wallets[0].adminkey }}" -H "X-Api-Key: {{ user.wallets[0].adminkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -170,7 +170,7 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url >curl -X GET {{ request.base_url
}}api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{ }}withdraw/api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -H "X-Api-Key: {{
user.wallets[0].inkey }}" user.wallets[0].inkey }}"
</code> </code>
</q-card-section> </q-card-section>
@@ -191,7 +191,7 @@
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}/withdraw/img/&lt;lnurl_id&gt;" >curl -X GET {{ request.base_url }}withdraw/img/&lt;lnurl_id&gt;"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
+5
View File
@@ -15,6 +15,7 @@ import lnbits.settings as settings
class Extension(NamedTuple): class Extension(NamedTuple):
code: str code: str
is_valid: bool is_valid: bool
is_admin_only: bool
name: Optional[str] = None name: Optional[str] = None
short_description: Optional[str] = None short_description: Optional[str] = None
icon: Optional[str] = None icon: Optional[str] = None
@@ -25,6 +26,7 @@ class Extension(NamedTuple):
class ExtensionManager: class ExtensionManager:
def __init__(self): def __init__(self):
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS]
self._extension_folders: List[str] = [ self._extension_folders: List[str] = [
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions")) x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
][0] ][0]
@@ -47,14 +49,17 @@ class ExtensionManager:
) as json_file: ) as json_file:
config = json.load(json_file) config = json.load(json_file)
is_valid = True is_valid = True
is_admin_only = True if extension in self._admin_only else False
except Exception: except Exception:
config = {} config = {}
is_valid = False is_valid = False
is_admin_only = False
output.append( output.append(
Extension( Extension(
extension, extension,
is_valid, is_valid,
is_admin_only,
config.get("name"), config.get("name"),
config.get("short_description"), config.get("short_description"),
config.get("icon"), config.get("icon"),
+1
View File
@@ -29,6 +29,7 @@ LNBITS_ALLOWED_USERS: List[str] = env.list(
"LNBITS_ALLOWED_USERS", default=[], subcast=str "LNBITS_ALLOWED_USERS", default=[], subcast=str
) )
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str) LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list( LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str "LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
) )
+37 -11
View File
@@ -111,7 +111,7 @@ window.LNbits = {
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
}, },
updateWallet: function (walletName, userId, walletId) { updateWallet: function (walletName, userId, walletId) {
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}` window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
}, },
deleteWallet: function (walletId, userId) { deleteWallet: function (walletId, userId) {
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
@@ -123,6 +123,7 @@ window.LNbits = {
[ [
'code', 'code',
'isValid', 'isValid',
'isAdminOnly',
'name', 'name',
'shortDescription', 'shortDescription',
'icon', 'icon',
@@ -135,7 +136,12 @@ window.LNbits = {
return obj return obj
}, },
user: function (data) { user: function (data) {
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets} var obj = {
id: data.id,
email: data.email,
extensions: data.extensions,
wallets: data.wallets
}
var mapWallet = this.wallet var mapWallet = this.wallet
obj.wallets = obj.wallets obj.wallets = obj.wallets
.map(function (obj) { .map(function (obj) {
@@ -153,16 +159,23 @@ window.LNbits = {
return obj return obj
}, },
wallet: function (data) { wallet: function (data) {
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey} newWallet = {
id: data.id,
name: data.name,
adminkey: data.adminkey,
inkey: data.inkey
}
newWallet.msat = data.balance_msat newWallet.msat = data.balance_msat
newWallet.sat = Math.round(data.balance_msat / 1000) newWallet.sat = Math.round(data.balance_msat / 1000)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat) newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
newWallet.sat
)
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('') newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
return newWallet return newWallet
}, },
payment: function (data) { payment: function (data) {
obj = { obj = {
checking_id:data.id, checking_id: data.id,
pending: data.pending, pending: data.pending,
amount: data.amount, amount: data.amount,
fee: data.fee, fee: data.fee,
@@ -174,8 +187,8 @@ window.LNbits = {
extra: data.extra, extra: data.extra,
wallet_id: data.wallet_id, wallet_id: data.wallet_id,
webhook: data.webhook, webhook: data.webhook,
webhook_status: data.webhook_status, webhook_status: data.webhook_status
} }
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000), new Date(obj.time * 1000),
@@ -225,7 +238,8 @@ window.LNbits = {
Quasar.plugins.Notify.create({ Quasar.plugins.Notify.create({
timeout: 5000, timeout: 5000,
type: types[error.response.status] || 'warning', type: types[error.response.status] || 'warning',
message: error.response.data.message || error.response.data.detail || null, message:
error.response.data.message || error.response.data.detail || null,
caption: caption:
[error.response.status, ' ', error.response.statusText] [error.response.status, ' ', error.response.statusText]
.join('') .join('')
@@ -331,17 +345,25 @@ window.windowMixin = {
} }
}, },
created: function () { created: function () {
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
this.g.allowedThemes = window.allowedThemes ?? ['classic'] if(this.$q.localStorage.getItem('lnbits.darkMode') == true || this.$q.localStorage.getItem('lnbits.darkMode') == false){
this.$q.dark.set(this.$q.localStorage.getItem('lnbits.darkMode'))
}
else{
this.$q.dark.set(true)
}
this.g.allowedThemes = window.allowedThemes ?? ['bitcoin']
// failsafe if admin changes themes halfway // failsafe if admin changes themes halfway
if (!this.$q.localStorage.getItem('lnbits.theme')){
this.changeColor(this.g.allowedThemes[0])
}
if ( if (
this.$q.localStorage.getItem('lnbits.theme') && this.$q.localStorage.getItem('lnbits.theme') &&
!this.g.allowedThemes.includes( !this.g.allowedThemes.includes(
this.$q.localStorage.getItem('lnbits.theme') this.$q.localStorage.getItem('lnbits.theme')
) )
) { ) {
console.log('allowedThemes changed by Admin', this.g.allowedThemes[0])
this.changeColor(this.g.allowedThemes[0]) this.changeColor(this.g.allowedThemes[0])
} }
@@ -368,6 +390,10 @@ window.windowMixin = {
.filter(function (obj) { .filter(function (obj) {
return !obj.hidden return !obj.hidden
}) })
.filter(function (obj) {
if (window.user.admin) return obj
return !obj.isAdminOnly
})
.map(function (obj) { .map(function (obj) {
if (user) { if (user) {
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1 obj.isEnabled = user.extensions.indexOf(obj.code) !== -1
+8
View File
@@ -7,6 +7,14 @@ $themes: (
marginal-bg: #1f2234, marginal-bg: #1f2234,
marginal-text: #fff marginal-text: #fff
), ),
'bitcoin': (
primary: #ff9853,
secondary: #ff7353,
dark: #2d293b,
info: #333646,
marginal-bg: #2d293b,
marginal-text: #fff
),
'mint': ( 'mint': (
primary: #3ab77d, primary: #3ab77d,
secondary: #27b065, secondary: #27b065,
+17 -4
View File
@@ -20,7 +20,7 @@
{% block head_scripts %}{% endblock %} {% block head_scripts %}{% endblock %}
</head> </head>
<body data-theme="classic"> <body data-theme="bitcoin">
<q-layout id="vue" view="hHh lpR lfr" v-cloak> <q-layout id="vue" view="hHh lpR lfr" v-cloak>
<q-header bordered class="bg-marginal-bg"> <q-header bordered class="bg-marginal-bg">
<q-toolbar> <q-toolbar>
@@ -67,8 +67,19 @@
icon="format_color_fill" icon="format_color_fill"
color="deep-purple" color="deep-purple"
size="md" size="md"
><q-tooltip>classic</q-tooltip> </q-btn ><q-tooltip>classic</q-tooltip>
><q-btn </q-btn>
<q-btn
v-if="g.allowedThemes.includes('bitcoin')"
dense
flat
@click="changeColor('bitcoin')"
icon="format_color_fill"
color="orange"
size="md"
><q-tooltip>bitcoin</q-tooltip>
</q-btn>
<q-btn
v-if="g.allowedThemes.includes('mint')" v-if="g.allowedThemes.includes('mint')"
dense dense
flat flat
@@ -151,11 +162,12 @@
</q-page> </q-page>
</q-page-container> </q-page-container>
{% endblock %} {% block footer %} {% endblock %} {% block footer %}
<q-footer <q-footer
class="bg-transparent q-px-lg q-py-md" class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}" :class="{'text-dark': !$q.dark.isActive}"
> >
<q-toolbar> <q-toolbar class="gt-sm">
<q-toolbar-title class="text-caption"> <q-toolbar-title class="text-caption">
{{ SITE_TITLE }}, {{SITE_TAGLINE}} {{ SITE_TITLE }}, {{SITE_TAGLINE}}
<br /> <br />
@@ -179,6 +191,7 @@
</q-btn> </q-btn>
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
{% endblock %} {% endblock %}
</q-layout> </q-layout>
+8 -12
View File
@@ -11,6 +11,7 @@ import base64
import hashlib import hashlib
from os import environ, error, getenv from os import environ, error, getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from .macaroon import load_macaroon, AESCipher
if imports_ok: if imports_ok:
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
@@ -58,12 +59,6 @@ def get_ssl_context(cert_path: str):
return context return context
def load_macaroon(macaroon_path: str):
with open(macaroon_path, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
def parse_checking_id(checking_id: str) -> bytes: def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(checking_id.replace("_", "/")) return base64.b64decode(checking_id.replace("_", "/"))
@@ -90,18 +85,19 @@ class LndWallet(Wallet):
self.port = int(getenv("LND_GRPC_PORT")) self.port = int(getenv("LND_GRPC_PORT"))
self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT") self.cert_path = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
macaroon_path = ( macaroon = (
getenv("LND_GRPC_MACAROON") getenv("LND_GRPC_MACAROON")
or getenv("LND_GRPC_ADMIN_MACAROON") or getenv("LND_GRPC_ADMIN_MACAROON")
or getenv("LND_ADMIN_MACAROON") or getenv("LND_ADMIN_MACAROON")
or getenv("LND_GRPC_INVOICE_MACAROON") or getenv("LND_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
) )
if macaroon_path.split(".")[-1] == "macaroon":
self.macaroon = load_macaroon(macaroon_path) encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
else: if encrypted_macaroon:
self.macaroon = macaroon_path macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read() cert = open(self.cert_path, "rb").read()
creds = grpc.ssl_channel_credentials(cert) creds = grpc.ssl_channel_credentials(cert)
+9 -1
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
from pydoc import describe
import httpx import httpx
import json import json
import base64 import base64
@@ -6,6 +7,7 @@ from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11 as lnbits_bolt11 from lnbits import bolt11 as lnbits_bolt11
from .macaroon import load_macaroon, AESCipher
from .base import ( from .base import (
StatusResponse, StatusResponse,
@@ -34,7 +36,13 @@ class LndRestWallet(Wallet):
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
or getenv("LND_REST_INVOICE_MACAROON") or getenv("LND_REST_INVOICE_MACAROON")
) )
self.auth = {"Grpc-Metadata-macaroon": macaroon}
encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
self.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = getenv("LND_REST_CERT") self.cert = getenv("LND_REST_CERT")
async def status(self) -> StatusResponse: async def status(self) -> StatusResponse:
+1
View File
@@ -0,0 +1 @@
from .macaroon import load_macaroon, AESCipher
+103
View File
@@ -0,0 +1,103 @@
from Cryptodome import Random
from Cryptodome.Cipher import AES
import base64
from hashlib import md5
import getpass
BLOCK_SIZE = 16
import getpass
def load_macaroon(macaroon: str) -> str:
"""Returns hex version of a macaroon encoded in base64 or the file path.
:param macaroon: Macaroon encoded in base64 or file path.
:type macaroon: str
:return: Hex version of macaroon.
:rtype: str
"""
# if the macaroon is a file path, load it
if macaroon.split(".")[-1] == "macaroon":
with open(macaroon, "rb") as f:
macaroon_bytes = f.read()
return macaroon_bytes.hex()
else:
# convert the bas64 macaroon to hex
try:
macaroon = base64.b64decode(macaroon).hex()
except:
pass
return macaroon
class AESCipher(object):
"""This class is compatible with crypto-js/aes.js
Encrypt and decrypt in Javascript using:
import AES from "crypto-js/aes.js";
import Utf8 from "crypto-js/enc-utf8.js";
AES.encrypt(decrypted, password).toString()
AES.decrypt(encrypted, password).toString(Utf8);
"""
def __init__(self, key=None, description=""):
self.key = key
self.description = description + " "
def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()
def unpad(self, data):
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
@property
def passphrase(self):
passphrase = self.key if self.key is not None else None
if passphrase is None:
passphrase = getpass.getpass(f"Enter {self.description}password:")
return passphrase
def bytes_to_key(self, data, salt, output=48):
# extended from https://gist.github.com/gsakkis/4546068
assert len(salt) == 8, len(salt)
data += salt
key = md5(data).digest()
final_key = key
while len(final_key) < output:
key = md5(key + data).digest()
final_key += key
return final_key[:output]
def decrypt(self, encrypted: str) -> str:
"""Decrypts a string using AES-256-CBC.
"""
passphrase = self.passphrase
encrypted = base64.b64decode(encrypted)
assert encrypted[0:8] == b"Salted__"
salt = encrypted[8:16]
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
try:
return self.unpad(aes.decrypt(encrypted[16:])).decode()
except UnicodeDecodeError:
raise ValueError("Wrong passphrase")
def encrypt(self, message: bytes) -> str:
passphrase = self.passphrase
salt = Random.new().read(8)
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
key = key_iv[:32]
iv = key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv)
return base64.b64encode(b"Salted__" + salt + aes.encrypt(self.pad(message))).decode()
# if this file is executed directly, ask for a macaroon and encrypt it
if __name__ == "__main__":
macaroon = input("Enter macaroon: ")
macaroon = load_macaroon(macaroon)
macaroon = AESCipher(description="encryption").encrypt(macaroon.encode())
print("Encrypted macaroon:")
print(macaroon)
+2 -1
View File
@@ -25,6 +25,7 @@ markupsafe==2.0.1
marshmallow==3.13.0 marshmallow==3.13.0
outcome==1.1.0 outcome==1.1.0
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
pycryptodomex==3.14.1
pydantic==1.8.2 pydantic==1.8.2
pypng==0.0.21 pypng==0.0.21
pyqrcode==1.2.1 pyqrcode==1.2.1
@@ -46,4 +47,4 @@ uvicorn==0.15.0
uvloop==0.16.0 uvloop==0.16.0
watchgod==0.7 watchgod==0.7
websockets==10.0 websockets==10.0
zipp==3.5.0 zipp==3.5.0
+18 -12
View File
@@ -9,18 +9,21 @@ from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl from tests.extensions.bleskomat.conftest import bleskomat, lnurl
from tests.mocks import WALLET from tests.mocks import WALLET
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_missing_secret(client): async def test_bleskomat_lnurl_api_missing_secret(client):
response = await client.get("/bleskomat/u") response = await client.get("/bleskomat/u")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Missing secret"} assert response.json() == {"status": "ERROR", "reason": "Missing secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_secret(client): async def test_bleskomat_lnurl_api_invalid_secret(client):
response = await client.get("/bleskomat/u?k1=invalid-secret") response = await client.get("/bleskomat/u?k1=invalid-secret")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid secret"} assert response.json() == {"status": "ERROR", "reason": "Invalid secret"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_unknown_api_key(client): async def test_bleskomat_lnurl_api_unknown_api_key(client):
query = { query = {
@@ -33,11 +36,12 @@ async def test_bleskomat_lnurl_api_unknown_api_key(client):
"f": "EUR", "f": "EUR",
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "xxx"# not checked, so doesn't matter signature = "xxx" # not checked, so doesn't matter
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Unknown API key"} assert response.json() == {"status": "ERROR", "reason": "Unknown API key"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
query = { query = {
@@ -51,10 +55,11 @@ async def test_bleskomat_lnurl_api_invalid_signature(client, bleskomat):
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = "invalid" signature = "invalid"
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"} assert response.json() == {"status": "ERROR", "reason": "Invalid API key signature"}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat): async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
query = { query = {
@@ -64,41 +69,42 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
"minWithdrawable": "1", "minWithdrawable": "1",
"maxWithdrawable": "1", "maxWithdrawable": "1",
"defaultDescription": "test valid sig", "defaultDescription": "test valid sig",
"f": "EUR",# tests use the dummy exchange rate provider "f": "EUR", # tests use the dummy exchange rate provider
} }
payload = query_to_signing_payload(query) payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature( signature = generate_bleskomat_lnurl_signature(
payload=payload, payload=payload, api_key_secret=bleskomat.api_key_secret, api_key_encoding=bleskomat.api_key_encoding
api_key_secret=bleskomat.api_key_secret,
api_key_encoding=bleskomat.api_key_encoding
) )
response = await client.get(f'/bleskomat/u?{payload}&signature={signature}') response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["tag"] == "withdrawRequest" assert data["tag"] == "withdrawRequest"
assert data["minWithdrawable"] == 1000 assert data["minWithdrawable"] == 1000
assert data["maxWithdrawable"] == 1000 assert data["maxWithdrawable"] == 1000
assert data["defaultDescription"] == "test valid sig" assert data["defaultDescription"] == "test valid sig"
assert data["callback"] == f'http://{HOST}:{PORT}/bleskomat/u' assert data["callback"] == f"http://{HOST}:{PORT}/bleskomat/u"
k1 = data["k1"] k1 = data["k1"]
lnurl = await get_bleskomat_lnurl(secret=k1) lnurl = await get_bleskomat_lnurl(secret=k1)
assert lnurl assert lnurl
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl): async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
secret = lnurl["secret"] secret = lnurl["secret"]
pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu" pr = "lntb500n1pseq44upp5xqd38rgad72lnlh4gl339njlrsl3ykep82j6gj4g02dkule7k54qdqqcqzpgxqyz5vqsp5h0zgewuxdxcl2rnlumh6g520t4fr05rgudakpxm789xgjekha75s9qyyssq5vhwsy9knhfeqg0wn6hcnppwmum8fs3g3jxkgw45havgfl6evchjsz3s8e8kr6eyacz02szdhs7v5lg0m7wehd5rpf6yg8480cddjlqpae52xu"
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "ERROR", "reason": "Failed to pay invoice: Insufficient balance."} assert response.json()["status"] == "ERROR"
assert ("Insufficient balance" in response.json()["reason"]) or ("fee" in response.json()["reason"])
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0 assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret) bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == True assert bleskomat_lnurl.has_uses_remaining() == True
WALLET.pay_invoice.assert_not_called() WALLET.pay_invoice.assert_not_called()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_bleskomat_lnurl_api_action_success(client, lnurl): async def test_bleskomat_lnurl_api_action_success(client, lnurl):
bleskomat = lnurl["bleskomat"] bleskomat = lnurl["bleskomat"]
@@ -111,7 +117,7 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 100000 assert wallet.balance_msat == 100000
WALLET.pay_invoice.reset_mock() WALLET.pay_invoice.reset_mock()
response = await client.get(f'/bleskomat/u?k1={secret}&pr={pr}') response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.json() == {"status": "OK"} assert response.json() == {"status": "OK"}
wallet = await get_wallet(bleskomat.wallet) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 50000 assert wallet.balance_msat == 50000